概述

在软件系统中,计算两个时间点之间的相对时间和持续时间是一个常见用例。例如,我们可能想要告诉用户自从在社交媒体平台上发布新图片后过去了多长时间,如“5分钟前”或“1年前”这样的时间描述。

尽管语义和词汇选择完全取决于上下文,但基本思想是一致的。本教程将探讨在Java中计算时间差的几种解决方案。考虑到Java 8引入了新的日期和时间API,我们将分别讨论Java 7和Java 8的解决方案。

1. Java 7

Java 7中包含多个与时间相关的类。由于Java 7 Date API的局限性,也存在许多第三方的时间和日期库。

1.1. 原生Java 7

首先,我们定义一个枚举(enum)来存储不同的时间粒度,并将其转换为毫秒:

public enum TimeGranularity {
    SECONDS {
        public long toMillis() {
            return TimeUnit.SECONDS.toMillis(1);
        }
    }, MINUTES {
        public long toMillis() {
            return TimeUnit.MINUTES.toMillis(1);
        }
    }, HOURS {
        public long toMillis() {
            return TimeUnit.HOURS.toMillis(1);
        }
    }, DAYS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(1);
        }
    }, WEEKS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(7);
        }
    }, MONTHS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(30);
        }
    }, YEARS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(365);
        }
    }, DECADES {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(365 * 10);
        }
    };

    public abstract long toMillis();
}

我们使用了java.util.concurrent.TimeUnit枚举,这是一个强大的时间转换工具。对于TimeGranularity枚举的每个值,我们重写了toMillis()抽象方法,以便返回相应值所代表的毫秒数。例如,“decade”对应3650天的毫秒数。

通过定义TimeGranularity枚举,我们可以定义两个方法。第一个方法接受一个java.util.Date对象和TimeGranularity实例,返回一个表示“时间差”的字符串:

static String calculateTimeAgoByTimeGranularity(Date pastTime, TimeGranularity granularity) {
    long timeDifferenceInMillis = getCurrentTime() - pastTime.getTime();
    return timeDifferenceInMillis / granularity.toMillis() + " " + 
      granularity.name().toLowerCase() + " ago";
}

这个方法将当前时间与给定时间之间的差值除以指定时间粒度的毫秒数,从而大致估算出从给定时间到现在的特定时间粒度内的经过时间。

我们使用了getCurrentTime()方法获取当前时间,为了测试,我们返回了一个固定的时刻,而不是从本地机器读取时间。在实际应用中,这个方法会使用System.currentTimeMillis()LocalDateTime.now().获取实际的当前时间。

让我们测试这个方法:

Assert.assertEquals("5 hours ago", 
  TimeAgoCalculator.calculateTimeAgoByTimeGranularity(
    new Date(getCurrentTime() - (5 * 60 * 60 * 1000)), TimeGranularity.HOURS));

此外,我们还可以编写一个方法,自动检测并返回最合适的合适时间粒度,提供更友好的输出:

static String calculateHumanFriendlyTimeAgo(Date pastTime) {
    long timeDifferenceInMillis = getCurrentTime() - pastTime.getTime();
    if (timeDifferenceInMillis / TimeGranularity.DECADES.toMillis() > 0) {
        return "several decades ago";
    } else if (timeDifferenceInMillis / TimeGranularity.YEARS.toMillis() > 0) {
        return "several years ago";
    } else if (timeDifferenceInMillis / TimeGranularity.MONTHS.toMillis() > 0) {
        return "several months ago";
    } else if (timeDifferenceInMillis / TimeGranularity.WEEKS.toMillis() > 0) {
        return "several weeks ago";
    } else if (timeDifferenceInMillis / TimeGranularity.DAYS.toMillis() > 0) {
        return "several days ago";
    } else if (timeDifferenceInMillis / TimeGranularity.HOURS.toMillis() > 0) {
        return "several hours ago";
    } else if (timeDifferenceInMillis / TimeGranularity.MINUTES.toMillis() > 0) {
        return "several minutes ago";
    } else {
        return "moments ago";
    }
}

现在,让我们看一个测试示例,展示如何使用:

Assert.assertEquals("several hours ago", 
  TimeAgoCalculator.calculateHumanFriendlyTimeAgo(new Date(getCurrentTime() - (5 * 60 * 60 * 1000))));

根据上下文,我们可以使用诸如“几”、“几个”、“许多”甚至确切的值等不同词汇。

1.2. Joda-Time库

在Java 8发布之前,Joda-Time是Java中处理各种时间和日期操作的事实标准。我们可以使用Joda-Time库的三个类来计算“时间差”:

  • org.joda.time.Period:它接受两个org.joda.time.DateTime对象,计算这两个时间点之间的差异。
  • org.joda.time.format.PeriodFormatter:定义了打印Period对象的格式。
  • org.joda.time.format.PeriodFormat:一个构建器类,用于创建自定义的PeriodFormatter

我们可以利用这三个类轻松获取现在和过去某个时间之间的确切时间差:

static String calculateExactTimeAgoWithJodaTime(Date pastTime) {
    Period period = new Period(new DateTime(pastTime.getTime()), new DateTime(getCurrentTime()));
    PeriodFormatter formatter = new PeriodFormatterBuilder().appendYears()
      .appendSuffix(" year ", " years ")
      .appendSeparator("and ")
      .appendMonths()
      .appendSuffix(" month ", " months ")
      .appendSeparator("and ")
      .appendWeeks()
      .appendSuffix(" week ", " weeks ")
      .appendSeparator("and ")
      .appendDays()
      .appendSuffix(" day ", " days ")
      .appendSeparator("and ")
      .appendHours()
      .appendSuffix(" hour ", " hours ")
      .appendSeparator("and ")
      .appendMinutes()
      .appendSuffix(" minute ", " minutes ")
      .appendSeparator("and ")
      .appendSeconds()
      .appendSuffix(" second", " seconds")
      .toFormatter();
    return formatter.print(period);
}

让我们看一个使用示例:

Assert.assertEquals("5 hours and 1 minute and 1 second", 
  TimeAgoCalculator.calculateExactTimeAgoWithJodaTime(new Date(getCurrentTime() - (5 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1 * 1000))));

也可以生成更友好的输出:

static String calculateHumanFriendlyTimeAgoWithJodaTime(Date pastTime) {
    Period period = new Period(new DateTime(pastTime.getTime()), new DateTime(getCurrentTime()));
    if (period.getYears() != 0) {
        return "several years ago";
    } else if (period.getMonths() != 0) {
        return "several months ago";
    } else if (period.getWeeks() != 0) {
        return "several weeks ago";
    } else if (period.getDays() != 0) {
        return "several days ago";
    } else if (period.getHours() != 0) {
        return "several hours ago";
    } else if (period.getMinutes() != 0) {
        return "several minutes ago";
    } else {
        return "moments ago";
    }
}

运行测试可以验证这个方法确实返回了更友好的“时间差”字符串:

Assert.assertEquals("several hours ago", 
  TimeAgoCalculator.calculateHumanFriendlyTimeAgoWithJodaTime(new Date(getCurrentTime() - (5 * 60 * 60 * 1000))));

同样,我们可以根据具体场景使用“一个”、“几个”或其他词汇。

1.3. Joda-Time时区

在Joda-Time库中,添加时区到“时间差”计算非常直接:

String calculateZonedTimeAgoWithJodaTime(Date pastTime, TimeZone zone) {
    DateTimeZone dateTimeZone = DateTimeZone.forID(zone.getID());
    Period period = new Period(new DateTime(pastTime.getTime(), dateTimeZone), new DateTime(getCurrentTimeByTimeZone(zone)));
    return PeriodFormat.getDefault().print(period);
}

getCurrentTimeByTimeZone()方法返回指定时区的当前时间值。为了测试,这个方法返回固定的时刻,但在实践中,应该使用Calendar.getInstance(zone).getTimeInMillis()LocalDateTime.now(zone).获取实际的当前时间。

2. Java 8

Java 8引入了全新的改进日期和时间API,它采纳了许多Joda-Time库的理念。我们可以使用原生的java.time.Durationjava.time.Period类来计算“时间差”:

static String calculateTimeAgoWithPeriodAndDuration(LocalDateTime pastTime, ZoneId zone) {
    Period period = Period.between(pastTime.toLocalDate(), getCurrentTimeByTimeZone(zone).toLocalDate());
    Duration duration = Duration.between(pastTime, getCurrentTimeByTimeZone(zone));
    if (period.getYears() != 0) {
        return "several years ago";
    } else if (period.getMonths() != 0) {
        return "several months ago";
    } else if (period.getDays() != 0) {
        return "several days ago";
    } else if (duration.toHours() != 0) {
        return "several hours ago";
    } else if (duration.toMinutes() != 0) {
        return "several minutes ago";
    } else if (duration.getSeconds() != 0) {
        return "several seconds ago";
    } else {
        return "moments ago";
    }
}

这段代码片段支持时区,仅使用Java 8的原生API。

3. PrettyTime库

PrettyTime是一个专为提供带国际化支持的“时间差”功能的强大库。它易于定制、使用方便,适用于Java 7和Java 8版本。

首先,我们需要将其依赖项添加到我们的pom.xml

<dependency>
    <groupId>org.ocpsoft.prettytime</groupId>
    <artifactId>prettytime</artifactId>
    <version>3.2.7.Final</version>
</dependency>

现在,获取更友好的“时间差”格式变得相当简单:

String calculateTimeAgoWithPrettyTime(Date pastTime) {
    PrettyTime prettyTime = new PrettyTime();
    return prettyTime.format(pastTime);
}

4. Time4J库

最后,Time4J是另一个优秀的Java中时间数据操作的库。它有一个PrettyTime类,可用于计算时间差。

添加其依赖项

<dependency>
    <groupId>net.time4j</groupId>
    <artifactId>time4j-base</artifactId>
    <version>5.9</version>
</dependency>
<dependency>
    <groupId>net.time4j</groupId>
    <artifactId>time4j-sqlxml</artifactId>
    <version>5.8</version>
</dependency>

添加此依赖后,计算时间差就很简单了:

String calculateTimeAgoWithTime4J(Date pastTime, ZoneId zone, Locale locale) {
    return PrettyTime.of(locale).printRelative(pastTime.toInstant(), zone);
}

与PrettyTime库一样,Time4J也支持内置的国际化。

5. 总结

在这篇文章中,我们讨论了在Java中计算时间差的不同方法。既有纯Java的解决方案,也有第三方库的选项。由于Java 8引入了新的日期和时间API,不同版本之前的纯Java解决方案有所不同。

如往常一样,所有示例代码可以在GitHub上找到。