前置知识

Java8 中使用 java.time.Duration java.time.Period 来对时间间隔(时间段)进行抽象,其中 Duration 类的基本单位是秒和纳秒,而 Period 则面向更长的时间段,基本单位是年、月、日。

另外,Java8 Time API 中还有一个类—— java.time.temporal.ChronoUnit 用于对时间单位建模,它是一个枚举类,包含了所有常见的基本时间单位。


一个错误的用法

接下来就是一个简单的需求,计算两个 LocalDateTime 之间的间隔天数:

LocalDateTime t1 = LocalDateTime.of(LocalDate.of(2023, 12, 12), LocalTime.MIN);
LocalDateTime t2 = LocalDateTime.of(LocalDate.of(2023, 12, 13), LocalTime.MIN);

// 如何计算出 t1 和 t2 之间的时间间隔?

然后,我似乎理所当然的调用了 Period 类的 getDays() 方法:

// Period 的基本单位是天,所以 LocalDateTime 需要转成 LocalDate
Period period = Period.between(t1.toLocalDate(), t2.toLocalDate());
System.out.println(period.getDays());

返回的结果是 1,2023 年 12 月 12 日和 2023 年 12 月 13 日确实相差一天,看起来似乎没问题。

但这里建立的前提是,我以为 getDays() 返回的是两日期之间间隔的秒数 / 86400 后,类似下面的代码:

long s1 = t1.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
long s2 = t2.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
System.out.println((s2 - s1) / 86400);

但是只要稍微改一下日期中的年份或者月份就不对了,比如,将 t1 中的月份改成 11 月,理论上,此时 t1 和 t2 的间隔天数应该是 31 天:

LocalDateTime t1 = LocalDateTime.of(LocalDate.of(2023, 11, 12), LocalTime.MIN);
LocalDateTime t2 = LocalDateTime.of(LocalDate.of(2023, 12, 13), LocalTime.MIN);

Period period = Period.between(t1.toLocalDate(), t2.toLocalDate());
// Period 算出来的结果居然是 1
System.out.println(period.getDays());

long s1 = t1.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
long s2 = t2.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
// 使用秒数计算,结果正常,是 31 天
System.out.println((s2 - s1) / 86400);

问题浮现出来了,Period 的建模是针对间隔时间的年,月,日,它的 getDays 并不是计算两个日期之间的总天数差,而是取出 Period 对象的 days 这个字段,我们把另外两个参数(年和月)也打印出来就能看到原因了:

LocalDateTime t1 = LocalDateTime.of(LocalDate.of(2023, 11, 12), LocalTime.MIN);
LocalDateTime t2 = LocalDateTime.of(LocalDate.of(2023, 12, 13), LocalTime.MIN);

Period period = Period.between(t1.toLocalDate(), t2.toLocalDate());
System.out.println("period: " + period);
System.out.println("years: " + period.getYears());
System.out.println("months: " + period.getMonths());
System.out.println("days: " + period.getDays());

打印结果如下:

period: P1M1D
years: 0
months: 1
days: 1

Period 的 months 和 days 都是 1,表示 2023 年 11 月 12 日和 2023 年 12 月 13 日,相差一个月多一天。

所以不能用 getDays() 获取实际间隔天数。

一些正确用法

第一种,就是上面提到的,将 LocalDate 转换成对应的时间戳,相减,然后除以 86400 得到天数。

第二种,是用 Period 获得的年月日参数,来计算天数,但这种方法很麻烦,因为需要考虑闰年和每个月的具体天数,非常麻烦,所以基本上不会使用。

第三种,使用 LocalDate 的 toEpochDay:

LocalDateTime t1 = LocalDateTime.of(LocalDate.of(2023, 11, 12), LocalTime.MIN);
LocalDateTime t2 = LocalDateTime.of(LocalDate.of(2023, 12, 13), LocalTime.MIN);
// 转换成 epoch day, 即相对于 1970 年 1 月 1 日的天数
long epochDay1 = t1.toLocalDate().toEpochDay();
long epochDay2 = t2.toLocalDate().toEpochDay();

System.out.println("epochDay1: " + epochDay1 + ", epochDay2: " + epochDay2);
System.out.println("Between days: " + (epochDay2 - epochDay1));

打印结果:

epochDay1: 19673, epochDay2: 19704
Between days: 31

第四种,使用 LocalDateTime 的 until() 方法:

LocalDateTime t1 = LocalDateTime.of(LocalDate.of(2023, 11, 12), LocalTime.MIN);
LocalDateTime t2 = LocalDateTime.of(LocalDate.of(2023, 12, 13), LocalTime.MIN);

System.out.println("Between Days: " + t1.until(t2, ChronoUnit.DAYS));

打印结果:

Between Days: 31

第五种,使用 ChronoUnit 枚举类的 between() 方法:

LocalDateTime t1 = LocalDateTime.of(LocalDate.of(2023, 11, 12), LocalTime.MIN);
LocalDateTime t2 = LocalDateTime.of(LocalDate.of(2023, 12, 13), LocalTime.MIN);

System.out.println("Between Days: " + ChronoUnit.DAYS.between(t1, t2));

打印结果:

Between Days: 31

结论

从简洁性来看,最推荐的是第四种和第五种方法,在业务代码中编写涉及到时间 API 的代码时,需要反复确认和测试,不然很容易引发意料之外的 BUG。