Java ZonedDateTime与英国夏令时行为验证及最佳实践咨询
先看你提供的测试代码和输出:
// 以下日期处于英国夏令时之外 ZonedDateTime utcFirstMarch = ZonedDateTime.of(2018, 3, 1, 12, 0, 0, 0, ZoneId.of("UTC")); ZonedDateTime londonFirstMarch = ZonedDateTime.of(2018, 3, 1, 12, 0, 0, 0, ZoneId.systemDefault()); // 以下日期处于英国夏令时之内 ZonedDateTime utcFirstMarchPlusMonth = ZonedDateTime.of(2018, 3, 1, 12, 0, 0, 0, ZoneId.of("UTC")).plusMonths(1); ZonedDateTime londonFirstMarchPlusMonth = ZonedDateTime.of(2018, 3, 1, 12, 0, 0, 0, ZoneId.systemDefault()).plusMonths(1); ZonedDateTime londonFirstMarchPlusMonthToUtc = ZonedDateTime.of(2018, 3, 1, 12, 0, 0, 0, ZoneId.systemDefault()).plusMonths(1).withZoneSameInstant(ZoneId.of("UTC"));
打印结果:
utcFirstMarch: 2018-03-01T12:00Z[UTC] londonFirstMarch: 2018-03-01T12:00Z[Europe/London] utcFirstMarchPlusMonth: 2018-04-01T12:00Z[UTC] londonFirstMarchPlusMonth: 2018-04-01T12:00+01:00[Europe/London] londonFirstMarchPlusMonthToUtc: 2018-04-01T11:00Z[UTC]
纪元秒输出:
utcFirstMarch: 1519905600 londonFirstMarch: 1519905600 utcFirstMarchPlusMonth: 1522584000 londonFirstMarchPlusMonth: 1522580400 londonFirstMarchPlusMonthToUtc: 1522580400
假设验证
我们逐个确认你的假设是否正确:
假设1:添加1个月会创建ZonedDateTime的新实例
完全正确。ZonedDateTime是Java的不可变类,所有修改方法(比如plusMonths())都会生成全新的实例,原实例的状态不会被改动——这是Java日期时间API保证线程安全的核心设计。假设2:无论新日期是否处于BST,ZonedDateTime的小时数始终保持不变
正确。plusMonths()是基于本地时间做日历调整的,它会保留原实例的时分秒信息,只修改月份(跨年时会调整年份)。从你的输出就能看到:londonFirstMarch是本地12:00,加一个月后londonFirstMarchPlusMonth仍然是本地12:00,只是时区偏移从UTC+0变成了BST的UTC+1。假设3:添加1个月后londonFirstMarchPlusMonth落入BST区间,为保持12:00的时间,实际会提前1小时,因此其纪元秒数与utcFirstMarchPlusMonth不同
正确。纪元秒是UTC纪元开始的绝对时间,伦敦进入BST后,本地12:00对应的UTC时间是11:00,所以londonFirstMarchPlusMonth的纪元秒比utcFirstMarchPlusMonth少了3600秒(1小时)——这正是夏令时偏移变化导致的:为了维持本地时间12:00,对应的绝对时间被迫提前了1小时。假设4:将londonFirstMarchPlusMonth转换为UTC时区后显示实际时间为11:00
完全正确。withZoneSameInstant()的作用就是保持绝对瞬间不变,只转换时区显示。伦敦的2018-04-01T12:00+01:00对应的UTC时间就是2018-04-01T11:00Z,和你的输出完全匹配。
关于“精确1个月后”的操作方式
你的思路方向是对的,但需要先明确需求:你要的“精确1个月后”是绝对时间(纪元秒)一致的日历月后,还是本地时间时分秒一致的日历月后?
如果是前者(追求绝对时间的精确性):
你的方案完全正确——应该优先在UTC时区下做日期调整(UTC没有夏令时、时区偏移变化的问题,不会出现时间跳转),最后再转换到Europe/London时区。这样操作的话,utcFirstMarchPlusMonth(2018-04-01T12:00Z)转换到伦敦时区后会是2018-04-01T13:00+01:00[Europe/London],它的纪元秒和UTC调整后的完全一致,保证了绝对时间的精确性。如果是后者(要维持本地时间的时分秒):
你原来的操作就是正确的,但要接受夏令时带来的绝对时间变化——就像你看到的,本地时间保持12:00,但对应的UTC时间提前了1小时。
另外需要澄清你补充说明里的一个小误解:plusMonths()是日历月份的调整,不是按24小时时长计算的。比如3月1日加1个月是4月1日(不管3月有31天),4月1日加1个月是5月1日(不管4月有30天)。如果你想要的是固定时长的“一个月”(比如30*24小时),那应该用plus(Duration.ofDays(30)),但这就不是日历意义上的“月份”了,而是固定天数的时长。
内容的提问来源于stack exchange,提问作者Taks

