跳到主要内容

· 阅读需 16 分钟
ZhongJianhao

LocalDateTime类是不包含时区信息的,可以通过atZone方法来设置ZoneId,返回ZonedDateTime类实例,通过atOffset方法来设置ZoneOffset,返回OffsetDateTime类实例。

ZonedDateTime 类注释详解

我们来逐段解读一下 ZonedDateTime 类的注释。

A date-time with a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30+01:00 Europe/Paris.

ISO-8601 日历系统中带时区的日期时间,例如2007-12-03T10:15:30+01:00 Europe/Paris

ISO-8601 我们简单理解为是规定日期和时间如何表示的标准即可,此处不深入研究。

2007-12-03T10:15:30+01:00 Europe/Paris中,2007-12-03为年月日;T是日期和时间组合表示时的固定写法,用于分隔;10:15:30是小时分钟秒;+01:00是此日期时间与 UTC 的时差为 +1 小时,即时区偏移(ZoneOffset)Europe/Paris是指此日期时间的所在区域为欧洲/巴黎,即时区 ID(ZoneId),time-zone ID 请查看:List of tz database time zones


ZonedDateTime is an immutable representation of a date-time with a time-zone. This class stores all date and time fields, to a precision of nanoseconds, and a time-zone, with a zone offset used to handle ambiguous local date-times. For example, the value "2nd October 2007 at 13:45.30.123456789 +02:00 in the Europe/Paris time-zone" can be stored in a ZonedDateTime.

ZonedDateTime 是带时区的日期时间的常量表示。此类存储所有的日期时间(精确到纳秒)和时区,其中时区偏移用于处理不明确的本地日期时间。 例如,值“2nd October 2007 at 13:45.30.123456789 +02:00 in the Europe/Paris time-zone” 可以被存储在ZonedDateTime中。

说它是常量是因为此类是被final修饰的,调用它的方法时,返回的都是新实例

“时区偏移用于处理不明确的本地日期时间”,之所以不明确,是因为夏令时的存在,同一地区,不同月份,甚至不同年份的 ZoneOffset 可能不一样。详见时区信息数据库

LocalDateTime localDateTime = LocalDateTime.of(2022, 1, 1, 0, 0);
// 设置时区 ID 为 美国/纽约
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("America/New_York"));
// 输出为 -05:00
System.out.println(zonedDateTime.getOffset());

// 设置月份为 4 月,即夏令时
zonedDateTime = zonedDateTime.withMonth(4);
// 输出为 -04:00
System.out.println(zonedDateTime.getOffset());

This class handles conversion from the local time-line of LocalDateTime to the instant time-line of Instant. The difference between the two time-lines is the offset from UTC/Greenwich, represented by a ZoneOffset.

此类处理LocalDateTime的本地时间线到Instant的瞬时时间线。两条时间线的差异是与 UTC/Greenwich 的偏移量,由ZoneOffset表示。

Instant(瞬时) 简单来说就是 java.time 中的时间戳(精度为纳秒),不包含时区信息。

此处说“两条时间线的差异是与 UTC/Greenwich 的偏移量”是指 Instant 加上 ZoneOffset 就可以获取 LocalDateTime。但如果是要将 Instant 转换为 ZonedDateTime 的话,需要设置 ZoneId。

Instant instant = Instant.now();
LocalDateTime localDateTime = instant.atOffset(ZoneOffset.ofHours(8)).toLocalDateTime();
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());

Converting between the two time-lines involves calculating the offset using the rules accessed from the ZoneId. Obtaining the offset for an instant is simple, as there is exactly one valid offset for each instant. By contrast, obtaining the offset for a local date-time is not straightforward. There are three cases:

  • Normal, with one valid offset. For the vast majority of the year, the normal case applies, where there is a single valid offset for the local date-time.
  • Gap, with zero valid offsets. This is when clocks jump forward typically due to the spring daylight savings change from "winter" to "summer". In a gap there are local date-time values with no valid offset.
  • Overlap, with two valid offsets. This is when clocks are set back typically due to the autumn daylight savings change from "summer" to "winter". In an overlap there are local date-time values with two valid offsets.

两条时间线之间的转换涉及到使用ZoneId访问规则(ZoneRules)计算偏移量。获取一个 instant 的偏移量很简单,因为每个 instant 正好有一个有效偏移量。相比之下,获取一个 local date-time 的有效偏移量 并不简单。有三种情况:

  • 正常:有一个有效的偏移量。在一年中的绝大多数时间里,local date-time 有一个有效的偏移量。
  • 间隙:没有有效的偏移量。这是由于春季夏令时从“冬季”到“夏季”,时钟被调快了,跳过了一段时间。在跳过的间隙中,有 local date-time,但没有有效的偏移量。
  • 重叠:有两个有效的偏移量。这是由于秋季夏令时从“夏季”到“冬季”,时钟往回调了一段时间。在重叠的情况下,有两个有效的偏移量的 local date-time。

Any method that converts directly or implicitly from a local date-time to an instant by obtaining the offset has the potential to be complicated.

For Gaps, the general strategy is that if the local date-time falls in the middle of a Gap, then the resulting zoned date-time will have a local date-time shifted forwards by the length of the Gap, resulting in a date-time in the later offset, typically "summer" time.

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.. Two additional methods, withEarlierOffsetAtOverlap() and withLaterOffsetAtOverlap(), help manage the case of an overlap.

任何通过获取偏移量显式或隐式地将 local date-time 转换为 instant 地方都有可能变得复杂。

对于间隙,一般策略是,如果 local date-time 落在间隙中间,那么产生的 zoned date-time 是 local date-time 向前移动间歇的长度后的,导致日期在较晚的偏移量,通常是“夏季”时间。

对于重叠,一般策略是,如果 local date-time 落在重叠中间,那么以前的偏移量将被保留。如果以前没有偏移量,或者以前的偏移量无效,那么就使用较早的偏移量,通常是“夏季”时间。两个额外的方法,withEarlierOffsetAtOverlap() and withLaterOffsetAtOverlap(),可以帮助管理重合的情况。

In terms of design, this class should be viewed primarily as the combination of a LocalDateTime and a ZoneId. The ZoneOffset is a vital, but secondary, piece of information, used to ensure that the class represents an instant, especially during a daylight savings overlap.

就设计而言,这个类应该主要被看作是LocalDateTimeZoneId的组合。ZoneOffset是一个重要但次要的信息,用来确保这个类代表一个瞬间,特别是在夏令时重叠的时候。

此处举例,美国夏令时一般在 3 月第二个周日 2AM 开始,将时钟拨快 1 小时,调到 3 点,那这一小时就是“冬季”到“夏季”的间隙(Gap);在 11 月第一个周日 2AM,又会将时钟拨慢 1 小时,调回到 1 点,那这一小时就是“夏季”到“冬季”的重叠(Overlap)

说“间歇没有有效的偏移量”,是因为落在间歇再转换为 zoned date-time 后,会自动加上间歇长度,所以那段时间只有 local date-time,没有偏移量。

ZoneId zoneId = ZoneId.of("America/New_York");
ZoneRules zoneRules = zoneId.getRules();
// 美国/纽约 2022 年的夏令时间歇时间为 03-13 02:00 ~ 03:00(不包含 03:00)
LocalDateTime localDateTime = LocalDateTime.of(2022, 3, 13, 2, 30);
System.out.println(zoneRules.getTransition(localDateTime));
System.out.println(zoneRules.getTransition(localDateTime.plusMinutes(30)));
// 如果时间落在间歇中间,那对应的 zoned date-time 会加上间歇长度(此处为 1 小时),同时时区偏移量为夏令时偏移量。
System.out.println(localDateTime.atZone(zoneId));

// 输出结果为:
Transition[Gap at 2022-03-13T02:00-05:00 to -04:00]
null
2022-03-13T03:30-04:00[America/New_York]

说“重叠有两个有效的偏移量”,是因为落在重叠时,如果以前没有偏移量或者以前的偏移量无效时,会使用较早的偏移量;如果以前有偏移量且更晚时,会使用较晚的偏移量。

ZoneId zoneId = ZoneId.of("America/New_York");
ZoneRules zoneRules = zoneId.getRules();
// 美国/纽约 2022 年的夏令时重叠时间为 11-06 01:00 ~ 02:00(不包含 02:00)
LocalDateTime localDateTime = LocalDateTime.of(2022, 11, 6, 1, 30);
System.out.println(zoneRules.getTransition(localDateTime));
System.out.println(zoneRules.getTransition(localDateTime.plusMinutes(30)));
// 如果时间落在重叠中间,以前没有偏移量时,会使用较早的偏移量,即夏令时偏移量 -04:00
System.out.println(localDateTime.atZone(zoneId));
// 如果时间落在重叠中间,以前有偏移量且更晚时,会使用较晚的偏移量,即非夏令时偏移量 -05:00
System.out.println(localDateTime.atOffset(ZoneOffset.ofHours(-6)).atZoneSameInstant(zoneId));

// 输出结果为:
Transition[Overlap at 2022-11-06T02:00-04:00 to -05:00]
null
2022-11-06T01:30-04:00[America/New_York]
2022-11-06T02:30-05:00[America/New_York]

说“以前的偏移量无效时”,是因为会有地区的时区规则发生变化的情况。比如太平洋中部岛国基里巴斯(Pacific/Kiritimati)修订国际日期变更线到其水域东边界限,从 1994 年 12 月 31 日开始,令划在 UTC-11 时区的凤凰群岛和 UTC-10 时区的莱恩群岛分别改用 UTC+13 和 UTC+14 时间,即比夏威夷时间早一整天,以消除与首都塔拉瓦慢 22 至 23 小时的不便。(Time in Kiribati

ZoneId zoneId = ZoneId.of("Pacific/Kiritimati");
ZoneRules zoneRules = zoneId.getRules();
LocalDateTime localDateTime = LocalDateTime.of(1994, 12, 31, 0, 0);
System.out.println(ZonedDateTime.of(localDateTime, zoneId));
System.out.println(zoneRules.getTransition(localDateTime.minusMinutes(1)));
// 以前的偏移量无效时,会使用较早的偏移量
System.out.println(localDateTime.minusMinutes(1).atZone(zoneId));

// 输出结果为:
1995-01-01T00:00+14:00[Pacific/Kiritimati]
null
1994-12-30T23:59-10:00[Pacific/Kiritimati]

This is a value-based class; use of identity-sensitive operations (including reference equality (==), identity hash code, or synchronization) on instances of ZonedDateTime may have unpredictable results and should be avoided. The equals method should be used for comparisons.

这是一个 value-based 类;在ZonedDateTime的实例上使用 identity-sensitive 的操作(包括引用相等==、identity hash code 或同步 synchronization)可能会产生不可预测的结果,所以要避免。应该使用 equals 方法来进行比较。

A ZonedDateTime holds state equivalent to three separate objects, a LocalDateTime, a ZoneId and the resolved ZoneOffset. The offset and local date-time are used to define an instant when necessary. The zone ID is used to obtain the rules for how and when the offset changes. The offset cannot be freely set, as the zone controls which offsets are valid.

一个ZonedDateTime相当于持有三个独立对象的状态,一个LocalDateTime,一个ZoneId和已解决的ZoneOffset。必要时,偏移量和 local date-time 被用来定义一个瞬间。zone ID 被用来获取偏移量及何时变化的规则。偏移量不能自由设置,因为时区控制着哪些偏移量时有效的。

This class is immutable and thread-safe.

这个类是不可变的,并且是线程安全的。

注释中说“or the previous offset is invalid”

withZoneSameLocal 和 withZoneSameInstant 的使用与区别

withZoneSameLocal 简介

先看 withZoneSameLocal 方法的注释:

Returns a copy of this date-time with a different time-zone, retaining the local date-time if possible.

This method changes the time-zone and retains the local date-time. The local date-time is only changed if it is invalid for the new zone, determined using the same approach as ofLocal(LocalDateTime, ZoneId, ZoneOffset).

To change the zone and adjust the local date-time, use withZoneSameInstant(ZoneId).

This instance is immutable and unaffected by this method call.

@param zone – the time-zone to change to, not null

@return a ZonedDateTime based on this date-time with the requested zone, not null

返回一个具有不同时区的 date-time 的副本,如果可能的话,保留 local date-time。

这个方法改变了时区并保留了 local date-time。在新的时区无效(同上文中偏移量无效)时,local date-time 才会被改变,与使用 ofLocal(LocalDateTime, ZoneId, ZoneOffset) 方法相同。

要改变区域并调整 local date-time,请使用 withZoneSameInstant(ZoneId)

被调用实例是不可变的,不受此方法调用的影响。

形参:zone - 要改变的时区,不为 null。

返回值:a ZonedDateTime - 基于该 date-time 的请求时区,不为 null。

简单来说就是修改时区但是保持 local date-time 不变:

LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime);
System.out.println(zonedDateTime.withZoneSameLocal(ZoneId.of("Europe/Moscow")));

// 输出结果为
2022-08-08T18:45:22.711+08:00[Asia/Shanghai]
2022-08-08T18:45:22.711+03:00[Europe/Moscow]

withZoneSameInstant 简介

再看 withZoneSameInstant 方法注释:

Returns a copy of this date-time with a different time-zone, retaining the instant.

This method changes the time-zone and retains the instant. This normally results in a change to the local date-time.

This method is based on retaining the same instant, thus gaps and overlaps in the local time-line have no effect on the result.

To change the offset while keeping the local time, use withZoneSameLocal(ZoneId).

@param zone – the time-zone to change to, not null

@return a ZonedDateTime based on this date-time with the requested zone, not null

@throws DateTimeException – if the result exceeds the supported date range

返回一个具有不同时区的 date-time 的副本,保留 instant。

这个方法改变了时区并保留了 instant。这通常会导致 local date-time 的改变。

这个方法是基于保留相同的 instant,因此本地时间线的间歇和重叠对结果没有影响。

要保持 local date-time 的同时改变偏移量,请使用 withZoneSameLocal(ZoneId)

形参zone - 要改变的时区,不为 null。

返回值:a ZonedDateTime - 基于该 date-time 的请求时区,不为 null。

抛出DateTimeException – 如果结果超出了支持的日期范围。

简单来说就是修改时区但是保持 instant 不变。因为时区变了,偏移量肯定变了,所以 local date-time 也会改变以保持 instant 不变。

LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime);
System.out.println(zonedDateTime.withZoneSameInstant(ZoneId.of("Europe/Moscow")));

// 输出结果为
2022-08-09T20:06:35.753+08:00[Asia/Shanghai]
2022-08-09T15:06:35.753+03:00[Europe/Moscow]

……

参考

· 阅读需 1 分钟
ZhongJianhao

Hello, World!

欢迎访问我的博客:欲穷三千界的博客

以下列出部分(封装第三方的不列)和 hutool(5.8+)的 JMH 对比测试(仅吞吐量,其他请查看源码),格式为:ZUtil 吞吐量 / hutool 吞吐量 = 快多少倍

  • 字符串转拼音:8.944 / 2.636 = 3.39