Java 8 日期时间:从 ZonedDateTime 获取一天的开始

Java 8 date-time: get start of day from ZonedDateTime

这些有什么区别吗:

zonedDateTime.truncatedTo(ChronoUnit.DAYS);

zonedDateTime.toLocalDate().atStartOfDay(zonedDateTime.getZone());

有什么理由偏爱其中之一?

谢谢

为更正更新:

在大多数情况下是相同的,请参阅以下巴西从冬季时间切换到夏季时间的示例:

ZonedDateTime zdt = 
  ZonedDateTime.of(2015, 10, 18, 0, 30, 0, 0, 
    ZoneId.of("America/Sao_Paulo")); // switch to summer time
ZonedDateTime zdt1 = zdt.truncatedTo(ChronoUnit.DAYS);
ZonedDateTime zdt2 = zdt.toLocalDate().atStartOfDay(zdt.getZone());

System.out.println(zdt); // 2015-10-18T01:30-02:00[America/Sao_Paulo]
System.out.println(zdt1); // 2015-10-18T01:00-02:00[America/Sao_Paulo]
System.out.println(zdt2); // 2015-10-18T01:00-02:00[America/Sao_Paulo]

截断发生在本地时间线上。如果您选择 DAYS,那么您选择午夜。根据 javadoctruncate() 方法最终转换回新的 ZonedDateTime 并将时间向前移动间隙的大小(1 小时)。

首先将 zdt 转换为 LocalDate(切断时间部分),然后在给定时区中寻找其 ZonedDateTime 部分对于这种情况实际上是相同的。

然而,对于从夏令时切换回冬令时的相反情况,有一个例外(非常感谢@Austin 给出了一个反例)。问题是在重叠期间何时决定使用哪个偏移量。通常 class ZonedDateTime 是 designed/specified 以使用先前的偏移量,另请参阅 Javadoc 的摘录:

For Overlaps, the general strategy is that if the local date-time falls in the middle of an Overlap, then the previous offset will be retained. If there is no previous offset, or the previous offset is invalid, then the earlier offset is used, typically "summer" time.

如果 class ZonedDateTime 会遵循自己的规范,那么这两个过程仍然是等价的:

zdt.truncatedTo(ChronoUnit.DAYS);

应该等同于

zdt.toLocalDate().atStartOfDay().atZone(zdt.getZone()).withEarlierOffsetAtOverlap();

但根据@Austin 的示例并由我在自己的测试中确认的真实行为是:

zdt.toLocalDate().atStartOfDay().atZone(zdt.getZone()).withLaterOffsetAtOverlap();

看起来 class ZonedDateTime 中隐藏的不一致,语气温和。如果您问我首选哪种方法,那么我宁愿提倡第二种方法,尽管它更长并且需要更多的击键。但它的一大优势是对其所做的事情更加透明。更喜欢第二种方法的另一个原因是:

它真正获得了本地时间等于一天开始的第一个时刻。否则,当使用第一种方法时,你必须写:

zdt.truncatedTo(ChronoUnit.DAYS).withEarlierOffsetAtOverlap();

它们略有不同。根据 javadocs,truncatedTo() will try to preserve the time zone in the case of overlap, but atStartOfDay() 将找到第一次出现的午夜。

例如,古巴在凌晨 1 点恢复夏令时,回到凌晨 12 点。如果您从该转换之后的时间开始,atStartOfDay() 将 return 第一次出现在凌晨 12 点,而 truncatedTo() 将 return 第二次出现。

ZonedDateTime zdt = ZonedDateTime.of(2016, 11, 6, 2, 0, 0, 0, ZoneId.of("America/Havana"));
ZonedDateTime zdt1 = zdt.truncatedTo(ChronoUnit.DAYS);
ZonedDateTime zdt2 = zdt.toLocalDate().atStartOfDay(zdt.getZone());
ZonedDateTime zdt3 = zdt.with(LocalTime.MIN);

System.out.println(zdt);  // 2016-11-06T02:00-05:00[America/Havana]
System.out.println(zdt1); // 2016-11-06T00:00-05:00[America/Havana]
System.out.println(zdt2); // 2016-11-06T00:00-04:00[America/Havana]
System.out.println(zdt3); // 2016-11-06T00:00-05:00[America/Havana]

请注意还有另一种方法:

zonedDateTime.with(LocalTime.MIN);

产生与 truncatedTo

相同的结果

zonedDateTime.truncatedTo(ChronoUnit.DAYS)是最佳选择。

正如 Austin 指出的那样,toLocalDate() 丢失了区域信息。来自 LocalDate 文档:

A date without a time-zone in the ISO-8601 calendar system, such as 2007-12-03. LocalDate is an immutable date-time object that represents a date, often viewed as year-month-day. Other date fields, such as day-of-year, day-of-week and week-of-year, can also be accessed. For example, the value "2nd October 2007" can be stored in a LocalDate.

除了 Austin 给出的示例之外,在开发和部署在不同机器上的常见情况下,这可能会导致问题。

举一个具体的例子,考虑处理纽约一家餐馆从美国东部时间上午 11 点到晚上 11 点收到的订单。如果代码是在纽约时间在机器上开发的,toLocalDate() 将不会显示任何错误。但是,当代码部署在 UTC 时区的服务器上时,它将在晚上 8 点后关闭(当 EDT 比 UTC 晚 4 小时时)。