1. 概述

本文将介绍如何在Java中计算两个日期的时间差,包括使用原生方法实现,以及引用第三方库的方式。

2. Java核心

2.1. 使用java.util.Date计算日期差

首先,我们使用Java核心API来计算两个日期之间相差的天数:

@Test
public void givenTwoDatesBeforeJava8_whenDifferentiating_thenWeGetSix()
  throws ParseException {
  
    // 日期解析
    SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH);
    Date firstDate = sdf.parse("06/24/2017");
    Date secondDate = sdf.parse("06/30/2017");
    
    // 计算时差
    long diffInMillies = Math.abs(secondDate.getTime() - firstDate.getTime());
    // 单位转为天数
    long diff = TimeUnit.DAYS.convert(diffInMillies, TimeUnit.MILLISECONDS);

    assertEquals(6, diff);
}

2.2. 使用java.time.temporal.ChronoUnit计算差异

Java 8中以TemporalUnit接口表示date-time单位,如秒或天。

每个单位都提供了一个名为between的方法实现,用于根据特定单位计算两个Temporal对象之间的时长。

例如,为了计算两个LocalDateTime之间的秒数:

@Test
public void givenTwoDateTimesInJava8_whenDifferentiatingInSeconds_thenWeGetTen() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime tenSecondsLater = now.plusSeconds(10);

    long diff = ChronoUnit.SECONDS.between(now, tenSecondsLater);

    assertEquals(10, diff);
}

ChronoUnit通过实现TemporalUnit接口提供了一系列具体的时长单位。强烈建议静态导入ChronoUnit枚举值以提高可读性:

import static java.time.temporal.ChronoUnit.SECONDS;

// 省略
long diff = SECONDS.between(now, tenSecondsLater);

此外,我们还可以将任何兼容的时间对象传递给between方法,甚至ZonedDateTime

ZonedDateTime的一个优点是,即使它们设置在不同的时区,计算也会正常进行:

@Test
public void givenTwoZonedDateTimesInJava8_whenDifferentiating_thenWeGetSix() {
    LocalDateTime ldt = LocalDateTime.now();
    ZonedDateTime now = ldt.atZone(ZoneId.of("America/Montreal"));
    ZonedDateTime sixMinutesBehind = now
      .withZoneSameInstant(ZoneId.of("Asia/Singapore"))
      .minusMinutes(6);
    
    long diff = ChronoUnit.MINUTES.between(sixMinutesBehind, now);
    
    assertEquals(6, diff);
}

2.3. 使用Temporal#until()

任何Temporal对象(如LocalDateZonedDateTime)都提供了until方法,用于根据指定单位计算到另一个Temporal对象的时间量:

@Test
public void givenTwoDateTimesInJava8_whenDifferentiatingInSecondsUsingUntil_thenWeGetTen() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime tenSecondsLater = now.plusSeconds(10);

    long diff = now.until(tenSecondsLater, ChronoUnit.SECONDS);

    assertEquals(10, diff);
}

Temporal#untilTemporalUnit#between是相同功能的不同API。

2.4. 使用java.time.Durationjava.time.Period

在Java 8中,时间API引入了两个新类:DurationPeriod

如果我们想以基于时间(小时、分钟或秒)的量计算两个日期-时间之间的差异,可以使用Duration类:

@Test
public void givenTwoDateTimesInJava8_whenDifferentiating_thenWeGetSix() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime sixMinutesBehind = now.minusMinutes(6);

    Duration duration = Duration.between(now, sixMinutesBehind);
    long diff = Math.abs(duration.toMinutes());

    assertEquals(6, diff);
}

然而,如果我们尝试使用Period类表示两个日期之间的差异,需要注意一个陷阱。

一个例子能快速解释这个问题。

让我们使用Period类计算两个日期之间的天数:

@Test
public void givenTwoDatesInJava8_whenUsingPeriodGetDays_thenWorks()  {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixDaysBehind = aDate.minusDays(6);

    Period period = Period.between(aDate, sixDaysBehind);
    int diff = Math.abs(period.getDays());

    assertEquals(6, diff);
}

如果运行上面的测试,它会通过。我们可能认为Period类可以方便地解决我们的问题。到目前为止,一切顺利。

如果这种方法对相差六天有效,我们不会怀疑它对相差60天也有效。

所以,让我们把测试中的6改为60,看看会发生什么:

@Test
public void givenTwoDatesInJava8_whenUsingPeriodGetDays_thenDoesNotWork() {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixtyDaysBehind = aDate.minusDays(60);

    Period period = Period.between(aDate, sixtyDaysBehind);
    int diff = Math.abs(period.getDays());

    assertEquals(60, diff);
}

再次运行测试,我们会看到:

java.lang.AssertionError: 
Expected :60
Actual   :29

哎呀!为什么Period类报告的差异为29天?

这是因为Period类以“x年,y个月和z天”的格式表示基于日期的时间量。当我们调用其getDays()方法时,它只返回“z天”部分。

因此,测试中的period对象的值为“0年,1个月和29天”:

@Test
public void givenTwoDatesInJava8_whenUsingPeriod_thenWeGet0Year1Month29Days() {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixtyDaysBehind = aDate.minusDays(60);
    Period period = Period.between(aDate, sixtyDaysBehind);
    int years = Math.abs(period.getYears());
    int months = Math.abs(period.getMonths());
    int days = Math.abs(period.getDays());
    assertArrayEquals(new int[] { 0, 1, 29 }, new int[] { years, months, days });
}

如果要在Java 8的时间API中计算日期之间的差异(以天为单位),ChronoUnit.DAYS.between()方法是最直接的方式。

3. 第三方库

3.1. Joda-Time

我们也可以使用Joda-Time进行相对直接的实现:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.12.5</version>
</dependency>

您可以从Maven中央仓库获取最新版本的Joda-time

对于LocalDate案例:

@Test
public void givenTwoDatesInJodaTime_whenDifferentiating_thenWeGetSix() {
    org.joda.time.LocalDate now = org.joda.time.LocalDate.now();
    org.joda.time.LocalDate sixDaysBehind = now.minusDays(6);

    long diff = Math.abs(Days.daysBetween(now, sixDaysBehind).getDays());
    assertEquals(6, diff);
}

类似地,对于LocalDateTime

@Test
public void givenTwoDateTimesInJodaTime_whenDifferentiating_thenWeGetSix() {
    org.joda.time.LocalDateTime now = org.joda.time.LocalDateTime.now();
    org.joda.time.LocalDateTime sixMinutesBehind = now.minusMinutes(6);

    long diff = Math.abs(Minutes.minutesBetween(now, sixMinutesBehind).getMinutes());
    assertEquals(6, diff);
}

3.2. Date4J

Date4j也提供了一个简单直接的实现——需要注意的是,在这种情况下,我们需要显式提供时区。

首先添加Maven依赖:

<dependency>
    <groupId>com.darwinsys</groupId>
    <artifactId>hirondelle-date4j</artifactId>
    <version>1.5.1</version>
</dependency>

这里有一个与标准DateTime一起工作的快速测试:

@Test
public void givenTwoDatesInDate4j_whenDifferentiating_thenWeGetSix() {
    DateTime now = DateTime.now(TimeZone.getDefault());
    DateTime sixDaysBehind = now.minusDays(6);
 
    long diff = Math.abs(now.numDaysFrom(sixDaysBehind));

    assertEquals(6, diff);
}

3.2. Hutool

Hutool 是一个国产工具库,对于中国开发者比较友好。

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.26</version>
</dependency>

使用 DateUtil 类的提供的 between 方法计算时间差

String dateStr1 = "2017-03-01 22:33:23";
Date date1 = DateUtil.parse(dateStr1);

String dateStr2 = "2017-04-01 23:33:23";
Date date2 = DateUtil.parse(dateStr2);

//相差一个月,31天
long betweenDay = DateUtil.between(date1, date2, DateUnit.DAY);

4. 总结

在这篇文章中,我们展示了在Java中(包括核心Java和第三分库)计算日期(有或无时间)之间差异的几种方法。