计算两个Java日期实例之间的差异

501

我在Scala中使用Java的java.util.Date类,想要比较一个Date对象和当前时间。我知道可以通过使用getTime()计算时间差:

(new java.util.Date()).getTime() - oldDate.getTime()

然而,这只给我留下了一个表示毫秒的long。是否有更简单、更好的方法来获得时间差?


12
为什么joda time没有得到更多的喜爱?如果你需要处理Java中的日期,它几乎是最佳选择。 - Doctor Jones
9
请检查我优雅的两行代码解决方案,不使用Joda库并将结果以任何TimeUnit显示,详情请见https://dev59.com/c3I_5IYBdhLWcg3wBub4#10650881。 - Sebastien Lorber
8
谴责那些推荐Joda时间库而不推荐真正的Java解决方案的人... - Zizouz212
5
关于推荐Joda-Time,Java捆绑的旧日期时间类很糟糕,真的很糟糕,设计差、令人困惑且麻烦。这些类非常糟糕以至于Joda-Time成为它们的替代品并取得了巨大成功。甚至Sun/Oracle也放弃了它们,并采用了java.time包作为Java 8及更高版本的一部分。java.time类受到Joda-Time的启发。Joda-Time和java.time都由同一个人Stephen Colbourne领导。我想说,“谁要是推荐使用DateCalendarSimpleDateFormat就太不应该了”。 - Basil Bourque
4
当问题提出时,Joda Time可能是一个很好的答案,但如今对于任何能使用Java 8的人来说,最好的答案是使用java.time.Period和/或Duration。请查看下面的Basil Bourque的答案。 - Ole V.V.
显示剩余4条评论
46个回答

616

简单的差异(无需库)

/**
 * Get a diff between two dates
 * @param date1 the oldest date
 * @param date2 the newest date
 * @param timeUnit the unit in which you want the diff
 * @return the diff value, in the provided unit
 */
public static long getDateDiff(Date date1, Date date2, TimeUnit timeUnit) {
    long diffInMillies = date2.getTime() - date1.getTime();
    return timeUnit.convert(diffInMillies,TimeUnit.MILLISECONDS);
}

然后你可以调用:

getDateDiff(date1,date2,TimeUnit.MINUTES);

获取两个日期之间的差异,以分钟为单位。

TimeUnitjava.util.concurrent.TimeUnit,它是一个标准的 Java 枚举类型,从纳秒到天数都有。


可读性强的差异比较(无需库)

public static Map<TimeUnit,Long> computeDiff(Date date1, Date date2) {

    long diffInMillies = date2.getTime() - date1.getTime();

    //create the list
    List<TimeUnit> units = new ArrayList<TimeUnit>(EnumSet.allOf(TimeUnit.class));
    Collections.reverse(units);

    //create the result map of TimeUnit and difference
    Map<TimeUnit,Long> result = new LinkedHashMap<TimeUnit,Long>();
    long milliesRest = diffInMillies;

    for ( TimeUnit unit : units ) {
        
        //calculate difference in millisecond 
        long diff = unit.convert(milliesRest,TimeUnit.MILLISECONDS);
        long diffInMilliesForUnit = unit.toMillis(diff);
        milliesRest = milliesRest - diffInMilliesForUnit;

        //put the result in the map
        result.put(unit,diff);
    }

    return result;
}

http://ideone.com/5dXeu6

输出结果类似于Map:{DAYS=1, HOURS=3, MINUTES=46, SECONDS=40, MILLISECONDS=0, MICROSECONDS=0, NANOSECONDS=0},并按单位排序。

您只需将该映射转换为用户友好的字符串即可。


警告

上述代码片段计算了两个时间点之间的简单差异。在夏令时转换期间可能会出现问题,如this post所述。这意味着如果您计算没有时间的日期之间的差异,可能会缺少一天/小时。

在我看来,日期差异有点主观,特别是在天数方面。您可以:

  • 计算经过的24小时时间数量:day+1 - day = 1 day = 24h

  • 计算经过的时间数量,注意夏令时:day+1 - day = 1 = 24h(但使用午夜时间和夏令时可能为0天23小时)

  • 计算“日切换”的数量,这意味着day+1 1pm - day 11am = 1 day,即使经过的时间只有2小时(如果有夏令时,则为1小时:p)

如果您对天数的日期差异定义与第一种情况相匹配,则我的答案是有效的。

使用JodaTime

如果您正在使用JodaTime,您可以通过以下方式获取两个瞬间(以ReadableInstant支持的毫秒)之间的差异:
Interval interval = new Interval(oldInstant, new Instant());

但是您也可以获取本地日期/时间的差异:

// returns 4 because of the leap year of 366 days
new Period(LocalDate.now(), LocalDate.now().plusDays(365*5), PeriodType.years()).getYears() 

// this time it returns 5
new Period(LocalDate.now(), LocalDate.now().plusDays(365*5+1), PeriodType.years()).getYears() 

// And you can also use these static methods
Years.yearsBetween(LocalDate.now(), LocalDate.now().plusDays(365*5)).getYears()

哇,你真的可以教老狗新把戏!以前从未听说过这个,将日期差异转换为有意义的字符串非常有用。 - Jeremy Goodell
@SebastienLorber TimeUnit中有一种方法可以这样计算差异吗?“闹钟将在3天、4小时和12分钟后设置”。 - likejudo
真是太棒了!真的为我节省了很多时间。感谢您的帮助。 - Lyju I Edwinson
我浪费了三天时间处理Java日期,最终只是把大部分代码扔掉并替换成了这个。谢谢。 - user1091524
这是我在StackOverflow上看到的最好的答案! :D:D:D - Cold
显示剩余3条评论

215

JDK中的Date API十分糟糕。建议使用Joda Time库.

Joda Time有一个时间间隔概念Interval:

Interval interval = new Interval(oldTime, new Instant());

编辑:顺便说一下,Joda有两个概念:Interval用于表示两个时间瞬间之间的时间间隔(表示上午8点到10点之间的时间),而Duration则表示没有实际时间边界的时间长度(例如表示两个小时!)

如果只关心时间比较,则大多数Date实现(包括JDK)都实现了Comparable接口,这允许您使用Comparable.compareTo()方法。


顺便提一下,你指的是 Comparable.compareTo() 而不是 Comparable.compare() - Scott Morrison
Joda-Time有三个类以各种方式描绘时间跨度:Interval、Duration和Period。此正确答案讨论了前两个类。有关Period的信息请参见我的回答 - Basil Bourque
Java 8有一个类似于Joda的新日期和时间API。 - tbodt
14
Java 8 中的新 java.time 包受 Joda-Time 的启发,但并非可以直接替换。它们各有优点和缺点。幸运的是,你不必在它们之间做出选择。只要小心使用 import 语句,就可以根据其强项使用它们。请参见这个答案以了解 java.time 的示例。 - Basil Bourque
7
FYI,Joda-Time 项目现在处于 maintenance mode,团队建议迁移到 java.time 类。请参见 Oracle 的教程 - Basil Bourque
如何将LocalDate转换为ReadableInstant? - Vishesh

161
int diffInDays = (int)( (newerDate.getTime() - olderDate.getTime()) 
                 / (1000 * 60 * 60 * 24) )

请注意,这适用于UTC日期,因此如果您查看本地日期,则差异可能会偏离一天。要使其在本地日期上正常工作需要完全不同的方法,因为涉及到夏令时。


6
在安卓系统中,这并不能正确工作,存在四舍五入误差。例如,5月19日到21日之间的天数会显示为1天,是因为将1.99转换成整数时向下取整了。在转换成整数前先进行四舍五入处理。 - Pratik Mandrekar
4
这是最好、最简单的答案。在计算两个日期之间的差异时,本地时区会相互抵消...因此正确的答案(作为double)只需通过 ((double)(newer.getTime() - older.getTime()) / (86400.0 * 1000.0)) 得出;作为double,您还可以得到小数天数,这很容易转换为HH:MM:ss。 - scottb
8
本地日期的问题在于夏令时的存在,这意味着有些日子可能有23或25个小时,可能会破坏结果。 - Michael Borgwardt
1
@Steve:嗯,不确定为什么之前我得到了不同的结果,但现在我可以重现你的错误。你代码中的大括号与我的答案不同,这导致(endDate.getTime() - startDate.getTime())被强制转换为int而不是最终结果,从而导致整数溢出。 - Michael Borgwardt
1
我只需要计算秒和毫秒来进行简单的性能时间检查,我使用了 double diffInSeconds = ((after.getTime() - b4.getTime()) / 1000d); - Joey Baruch
显示剩余5条评论

78

使用Java 8+内置的java.time框架:

ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime oldDate = now.minusDays(1).minusMinutes(10);
Duration duration = Duration.between(oldDate, now);
System.out.println("ISO-8601: " + duration);
System.out.println("Minutes: " + duration.toMinutes());

输出:

ISO-8601: PT24H10M

分钟:1450

更多信息,请参见Oracle教程ISO 8601标准。


7
对于日期,可以使用“Period”代替“Duration”。 - Aphex
很好 :) 我更喜欢这个 - Deunz

68

简短总结:

将过时的java.util.Date对象转换为其替代品java.time.Instant。然后使用Duration计算经过的时间。

Duration d = 
    Duration.between(                   // Calculate the span of time between two moments as a number of hours, minutes, and seconds.
        myJavaUtilDate.toInstant() ,    // Convert legacy class to modern class by calling new method added to the old class.
        Instant.now()                   // Capture the current moment in UTC. About two and a half hours later in this example.
    )
;

d.toString(): PT2H34M56S
d.toMinutes(): 154
d.toMinutesPart(): 34

ISO 8601格式: PnYnMnDTnHnMnS

明智的标准ISO 8601将时间段定义为一定数量的年、月、日、小时等,并将这样的时间段称为持续时间。格式为PnYnMnDTnHnMnS,其中P表示“周期”,T分隔日期部分和时间部分,中间是数字后面跟着一个字母。

示例:

  • P3Y6M4DT12H30M5S三年零六个月零四天十二小时三十分钟五秒钟
  • PT4H30M四小时半

java.time

Java 8及以后内置的java.time框架取代了问题多多的旧版java.util.Date/java.util.Calendar类。新类受到广受欢迎的Joda-Time框架的启发,是其后继者,在概念上相似但重新架构。由JSR 310定义。由ThreeTen-Extra项目扩展。请参见Tutorial

Moment

Instant类表示时间线上以UTC为基准的一个瞬间,精度为纳秒(最多九位十进制小数)。Instant类。

其中,UTC表示协调世界时,nanoseconds表示纳秒。
Instant instant = Instant.now() ;  // Capture current moment in UTC.

最好避免使用旧的类,如Date/Calendar。但如果必须与尚未更新为java.time的旧代码进行互操作,则需要进行相互转换。调用添加到旧类中的新转换方法。要将java.util.Date转换为Instant,请调用Date::toInstant
Instant instant = myJavaUtilDate.toInstant() ;  // Convert from legacy `java.util.Date` class to modern `java.time.Instant` class.

时间跨度
java.time类将表示一段时间的想法分为两个部分,即按照年、月、日计数的时间跨度和按照日、小时、分钟、秒计数的持续时间:
  • Period 表示年、月、日
  • Duration 表示日、小时、分钟、秒
以下是一个示例。
ZoneId zoneId = ZoneId.of ( "America/Montreal" );
ZonedDateTime now = ZonedDateTime.now ( zoneId );
ZonedDateTime future = now.plusMinutes ( 63 );
Duration duration = Duration.between ( now , future );

将文本转换为中文:

在控制台中输出。

PeriodDuration 都使用 ISO 8601 标准来生成它们的值的字符串表示形式。

System.out.println ( "now: " + now + " to future: " + now + " = " + duration );

现在时间:2015年11月26日00:46:48.016-05:00 [美国/蒙特利尔],未来时间:2015年11月26日00:46:48.016-05:00 [美国/蒙特利尔] = PT1H3M。
Java 9 添加了一些方法到 Duration 中,可以获取天数、小时数、分钟数和秒数的部分。
您可以获取整个 Duration 的总天数、小时数、分钟数、秒数、毫秒数或纳秒数。
long totalHours = duration.toHours();

在Java 9中,Duration类新增了一些方法,用于返回天、小时、分钟、秒、毫秒/纳秒等不同部分。调用to...Part方法:如toDaysPart()toHoursPart()等。

ChronoUnit

如果您只关心更简单、更大的时间粒度,比如“经过的天数”,可以使用ChronoUnit枚举。
long daysElapsed = ChronoUnit.DAYS.between( earlier , later );

另一个例子。
Instant now = Instant.now();
Instant later = now.plus( Duration.ofHours( 2 ) );
…
long minutesElapsed = ChronoUnit.MINUTES.between( now , later );

120


关于java.time:

java.time框架内置于Java 8及其后续版本。这些类替代了老旧的传统日期时间类,如java.util.DateCalendarSimpleDateFormat

Joda-Time项目现已进入维护模式,建议迁移到java.time。

要了解更多,请查看Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范是JSR 310
如何获取java.time类? 该项目是通过添加额外的类来扩展Java.time的ThreeTen-Extra项目。该项目是Java.time可能未来增加的一些内容的试验场所。您可能会在这里找到一些有用的类,例如IntervalYearWeekYearQuarter更多

Joda-Time

更新:Joda-Time 项目现已进入 维护模式,团队建议迁移到 java.time 类。为了历史记录,我保留本节内容。

Joda-Time 库使用 ISO 8601 作为默认设置。它的 Period 类解析和生成这些 PnYnMnDTnHnMnS 字符串。

DateTime now = DateTime.now(); // Caveat: Ignoring the important issue of time zones.
Period period = new Period( now, now.plusHours( 4 ).plusMinutes( 30));
System.out.println( "period: " + period );

翻译:渲染:
period: PT4H30M

很有趣,你用“tl; dr”开始了一个与问题无关的长篇文字。问题不是关于如何从X时间之前或X时间单位中获取时间戳,而是如何获取两个日期之间的差值。就这样。 - Buffalo
@Buffalo,我不明白您的评论。我的回答涉及PeriodDurationChronoUnit三个类,它们的目的是确定时间点之间的差距。请指出我“冗长的文字”中哪些部分是不相关的。而且您说问题要求“日期之间的差异”是不正确的:问题要求在Date对象之间计算时间差,这些对象是日期时间时刻,而不是“日期”,尽管该类的命名不幸地具有这种含义。 - Basil Bourque
你的评论中没有处理两个已存在的java.util.Date对象的内容。我尝试在我的代码中使用各种片段,但没有找到使用java.util.Date对象的内容。 - Buffalo
@Buffalo 很好,批评得当。我在“tl;dr”部分添加了代码,将java.util.Date转换为Instant(只需调用.toInstant)。我还添加了“Moment”部分来解释。您提到了一对Date对象,但实际上问题仅使用单个现有的Date对象,然后捕获当前时刻,所以我也这样做了。感谢反馈!提示:放弃使用Date - 这些遗留类真的是一团糟。 - Basil Bourque

54

你需要更明确地定义你的问题。例如,你可以获取两个Date对象之间的毫秒数并将其除以24小时的毫秒数,但是:

  • 这不会考虑时区——Date对象始终使用协调世界时(UTC)。
  • 这也不会考虑夏令时的影响(有些日子只有23小时,例如)。
  • 即使在UTC时间内,从8月16日晚上11点到8月18日凌晨2点有多少天?这只有27个小时,那么是一天吗?还是应该算三天,因为涵盖了三个日期?

1
我曾认为java.util.Date只是一个围绕时间毫秒表示的小包装器,解释为本地(默认)时区。打印Date会给我一个本地时区的表示。我是否混淆了这里的概念? - Adriaan Koster

39

2
FYI,Joda-Time 项目现已进入 维护模式,团队建议迁移至 java.time 类。 - Basil Bourque

24

使用毫秒级别的方法可能会在某些语言环境下导致问题。

例如,假设有两个日期:03/24/2007 和 03/25/2007,它们之间相差1天;

然而,在英国运行时,如果使用毫秒级别的方法,你将得到0天。

/** Manual Method - YIELDS INCORRECT RESULTS - DO NOT USE**/  
/* This method is used to find the no of days between the given dates */  
public long calculateDays(Date dateEarly, Date dateLater) {  
   return (dateLater.getTime() - dateEarly.getTime()) / (24 * 60 * 60 * 1000);  
} 

更好的实现方式是使用java.util.Calendar

/** Using Calendar - THE CORRECT WAY**/  
public static long daysBetween(Calendar startDate, Calendar endDate) {  
  Calendar date = (Calendar) startDate.clone();  
  long daysBetween = 0;  
  while (date.before(endDate)) {  
    date.add(Calendar.DAY_OF_MONTH, 1);  
    daysBetween++;  
  }  
  return daysBetween;  
}  

19
请注明这段代码和第三句话的原作者,其博客文章发布日期为2007年 - wchargin
这不是有点低效吗? - Gangnus
它不能正常工作。daysBetween(new GregorianCalendar(2014,03,01), new GregorianCalendar(2014,04,02))); 返回31,但应该返回32:http://www.timeanddate.com/date/durationresult.html?d1=01&m1=03&y1=2014&d2=02&m2=04&y2=2014 - marcolopes
5
@marcolopes — 你错了,因为日历月份是从零开始计算的。我相信你想表达三月/四月,但你正在测试四月/六月,这是31天。为了保险起见,建议这样写 -> new GregorianCalendar(2014, Calendar.MARCH, 1)… - Uncle Iroh
@UncleIroh,你是对的!我错过了一个重要的事实(日历月份是从零开始计数)。我必须重新审查这个问题。 - marcolopes
警告:如果时间字段不为零并且跨越夏令时边界,则此方法可能返回不正确的结果。 - Jool

24

一个稍微简单一点的替代方案:

System.currentTimeMillis() - oldDate.getTime()

"更好看"的问题是:你需要什么样的效果?将时间段表示为小时、天数等可能会导致误差和错误的预期,因为日期的复杂性(例如,夏令时可能导致一天有23或25个小时)。


24

由于这里的所有答案都是正确的,但使用传统的Java或第三方库如joda或类似的库,因此我将介绍另一种方法,使用Java 8及更高版本中的新java.time类。请参见Oracle教程

使用LocalDateChronoUnit

LocalDate d1 = LocalDate.of(2017, 5, 1);
LocalDate d2 = LocalDate.of(2017, 5, 18);

long days = ChronoUnit.DAYS.between(d1, d2);
System.out.println( days );

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接