在 Java 8 引入的日期时间 API 中,Period.between 是用来计算两个 LocalDate 之间年、月、日差距的工具方法。它和 ChronoUnit.DAYS.between 这类基于时间线总长度的算法不同,采用的是日历语义下的分段计算。理解它的内部逻辑,对于写对账、计费、周期任务等系统非常关键。

Period.between 的基本行为
Period.between 接收两个 LocalDate 参数,返回一个 Period 对象,其中包含相差的年数、月数和天数。它的设计目标是反映人们日常生活中说的“差几年几月几天”,而不是机器视角下的绝对时间间隔。
下面是一段最简单的使用示例:
import java.time.LocalDate;
import java.time.Period;
public class PeriodDemo {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2020, 1, 15);
LocalDate end = LocalDate.of(2023, 5, 20);
Period period = Period.between(start, end);
// 输出 P3Y4M5D
System.out.println(period);
System.out.println("年:" + period.getYears());
System.out.println("月:" + period.getMonths());
System.out.println("日:" + period.getDays());
}
}
从输出可以看到,结果并不是总天数折算,而是先算整年,再算整月,最后算剩余天数。这种分段方式符合人类习惯,但在程序自动化处理时需要特别注意它的边界表现。
核心计算逻辑:逐级递减
在 OpenJDK 的实现中,Period.between 的计算顺序固定为:先尝试用结束日期减去开始日期的整年,再在剩余部分减去整月,最后剩下的不足一月的天数作为日差。这意味着月份的计算依赖具体月份的长度。
例如从 2023-01-31 到 2023-02-28,由于 2 月没有 31 天,无法构成完整的一个月,因此月份差为 0,天数差为 28。如果改成 2023-01-31 到 2023-03-31,则月份差为 2,天数差为 0。这种“对齐失败就退化为天数”的行为,是很多业务 bug 的来源。
import java.time.LocalDate;
import java.time.Period;
public class EdgeCase {
public static void main(String[] args) {
LocalDate d1 = LocalDate.of(2023, 1, 31);
LocalDate d2 = LocalDate.of(2023, 2, 28);
Period p = Period.between(d1, d2);
// 输出 P0Y0M28D,而不是 P0Y1M0D
System.out.println(p);
LocalDate d3 = LocalDate.of(2024, 2, 29);
LocalDate d4 = LocalDate.of(2024, 3, 29);
Period p2 = Period.between(d3, d4);
// 闰年2月29到3月29,输出 P0Y1M0D
System.out.println(p2);
}
}
上面的代码展示了月末与闰年场景下的差异。在闰年之后的非闰年,类似 2 月 29 日到次年 2 月 28 日也不会被算作整年整月,而是拆出天数。开发者若用其做“是否满一年”的判断,必须额外处理这类边界。
与 ChronoUnit 的对比
如果业务只关心总天数或总月数,应该使用 ChronoUnit 提供的基于时间刻度的方法。它不考虑日历对齐,只做数学上的差值换算。
| 场景 | Period.between | ChronoUnit.MONTHS |
|---|---|---|
| 2023-01-31 到 2023-02-28 | 0月28天 | 1个月 |
| 2023-01-15 到 2023-02-15 | 1个月0天 | 1个月 |
| 2024-02-29 到 2025-02-28 | 0年28天(因日不对齐) | 12个月 |
从表中能看出,ChronoUnit 更适合做“经过了多少个日历单位”的统计,而 Period.between 更适合展示“还差多少达到某个生日或纪念日”。选错 API 会让报表数据和用户预期不一致。
实际开发中的避坑建议
第一,不要使用 Period.between 的月数或天数直接做利息、租金等按比例分摊的计算。这类场景通常需要固定按 30 天或实际天数折算,应使用 ChronoUnit.DAYS.between 配合 BigDecimal 处理。
第二,当用 Period 判断会员是否满一年时,建议统一将开始日期加上 Period 再比较,而不是直接读年月日字段。例如 start.plus(Period.between(start, end)) 是否等于 end,能规避月末偏移问题。
import java.time.LocalDate;
import java.time.Period;
public class SafeCheck {
public static boolean isExactAnniversary(LocalDate start, LocalDate end) {
if (end.isBefore(start)) {
return false;
}
Period p = Period.between(start, end);
// 将差值加回开始日期,看是否能精确到达结束日期
LocalDate reconstructed = start.plus(p);
return reconstructed.equals(end);
}
public static void main(String[] args) {
LocalDate s = LocalDate.of(2023, 1, 31);
LocalDate e = LocalDate.of(2024, 1, 31);
System.out.println(isExactAnniversary(s, e)); // true
LocalDate e2 = LocalDate.of(2024, 1, 30);
System.out.println(isExactAnniversary(s, e2)); // false
}
}
这种写法能明确识别出“是否刚好满整年整月”,比单纯比较 getYears() == 1 && getMonths() == 0 更安全。因为后者在 1 月 31 到次年 1 月 30 时也会误判为满一年差一天。
总结
Period.between 的底层是日历语义下的年月日逐级拆分,不是线性时间差。它在处理月末、闰年、不同月份长度时,会保留无法对齐的天数,而不是四舍五入成月或年。开发时应先想清楚业务需要的是“人类语言上的差距”还是“机器刻度上的差距”,再决定用 Period 还是 ChronoUnit。在涉及钱、合同、资格期限的系统里,推荐用加回比较法来验证周期边界,减少隐蔽的逻辑错误。
JavaPeriod_between日期计算修改时间:2026-08-04 11:54:31