ZonedDateTime 比较:预期:[Etc/UTC] 但实际为:[UTC]

ZonedDateTime comparison: expected: [Etc/UTC] but was: [UTC]

我正在比较两个似乎相等的日期,但它们包含不同名称的区域:一个是 Etc/UTC,另一个是 UTC

根据这个问题:Is there a difference between the UTC and Etc/UTC time zones? - 这两个区域是相同的。但是我的测试失败了:

import org.junit.Test;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.junit.Assert.assertEquals;

public class TestZoneDateTime {

    @Test
    public void compareEtcUtcWithUtc() {
        ZonedDateTime now = ZonedDateTime.now();
        ZonedDateTime zoneDateTimeEtcUtc = now.withZoneSameInstant(ZoneId.of("Etc/UTC"));
        ZonedDateTime zoneDateTimeUtc = now.withZoneSameInstant(ZoneId.of("UTC"));

        // This is okay
        assertEquals(Timestamp.from(zoneDateTimeEtcUtc.toInstant()), Timestamp.from(zoneDateTimeUtc.toInstant()));
        // This one fails
        assertEquals(zoneDateTimeEtcUtc,zoneDateTimeUtc);

        // This fails as well (of course previous line should be commented!)
        assertEquals(0, zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc));
    }
}

结果:

java.lang.AssertionError: 
Expected :2018-01-26T13:55:57.087Z[Etc/UTC]
Actual   :2018-01-26T13:55:57.087Z[UTC]

更具体地说,我希望 ZoneId.of("UTC") 等于 ZoneId.of("Etc/UTC"),但它们不是!

作为@NicolasHenneaux ,我可能应该使用compareTo(...) 方法。这是个好主意,但是 zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc) returns -16 值,因为 ZoneDateTime:

中的这个实现
cmp = getZone().getId().compareTo(other.getZone().getId());

断言结果:

java.lang.AssertionError: 
Expected :0
Actual   :-16

所以问题出在ZoneId实现的某个地方。但我仍然希望,如果两个区域 ID 都有效并且都指定同一个区域,那么它们 应该 相等。

我的问题是:这是一个库错误,还是我做错了什么?

更新

几个人试图说服我这是正常行为,比较方法的实现使用正常String id表示ZoneId。在这种情况下我应该问,为什么下面的测试运行正常?

    @Test
    public void compareUtc0WithUtc() {
        ZonedDateTime now = ZonedDateTime.now();
        ZoneId utcZone = ZoneId.of("UTC");
        ZonedDateTime zonedDateTimeUtc = now.withZoneSameInstant(utcZone);
        ZoneId utc0Zone = ZoneId.of("UTC+0");
        ZonedDateTime zonedDateTimeUtc0 = now.withZoneSameInstant(utc0Zone);

        // This is okay
        assertEquals(Timestamp.from(zonedDateTimeUtc.toInstant()), Timestamp.from(zonedDateTimeUtc0.toInstant()));
        assertEquals(0, zonedDateTimeUtc.compareTo(zonedDateTimeUtc0));
        assertEquals(zonedDateTimeUtc,zonedDateTimeUtc0);
    }

如果Etc/UTCUTC一样,那么我看到两个选项:

否则我不明白为什么 UTC+0UTC 工作正常。

UPDATE-2 我已经报告了一个错误,ID : 9052414。看看 Oracle 团队的决定。

UPDATE-3 错误报告已接受(不知道他们会不会将其关闭为 "won't fix"):https://bugs.openjdk.java.net/browse/JDK-8196398

ZoneDateTime如果要比较时间的话要转成OffsetDateTime再和compareTo(..)比较。

ZoneDateTime.equalsZoneDateTime.compareTo 比较时间和时区标识符,即标识时区的第 t 个字符串。

ZoneDateTime 是一个瞬间和一个区域(有它的 id 而不仅仅是一个偏移量),而 OffsetDateTime 是一个瞬间和一个区域偏移量。如果你想比较两个ZoneDateTime对象之间的时间,你应该使用OffsetDateTime.

方法ZoneId.of(..)正在解析您提供的字符串并在需要时对其进行转换。 ZoneId 表示时区而不是偏移量,即与 GMT 时区的时差。而 ZoneOffset 代表偏移量。 https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#of-java.lang.String-

If the zone ID equals 'GMT', 'UTC' or 'UT' then the result is a ZoneId with the same ID and rules equivalent to ZoneOffset.UTC.

If the zone ID starts with 'UTC+', 'UTC-', 'GMT+', 'GMT-', 'UT+' or 'UT-' then the ID is a prefixed offset-based ID. The ID is split in two, with a two or three letter prefix and a suffix starting with the sign. The suffix is parsed as a ZoneOffset. The result will be a ZoneId with the specified UTC/GMT/UT prefix and the normalized offset ID as per ZoneOffset.getId(). The rules of the returned ZoneId will be equivalent to the parsed ZoneOffset.

All other IDs are parsed as region-based zone IDs. Region IDs must match the regular expression [A-Za-z][A-Za-z0-9~/._+-]+ otherwise a DateTimeException is thrown. If the zone ID is not in the configured set of IDs, ZoneRulesException is thrown. The detailed format of the region ID depends on the group supplying the data. The default set of data is supplied by the IANA Time Zone Database (TZDB). This has region IDs of the form '{area}/{city}', such as 'Europe/Paris' or 'America/New_York'. This is compatible with most IDs from TimeZone.

所以UTC+0被转化为UTC而Etc/UTC保持不变

ZoneDateTime.compareTo比较id的字符串("Etc/UTC".compareTo("UTC") == 16)。这就是你应该使用 OffsetDateTime.

的原因

class ZonedDateTime 在其 equals() 方法中使用内部 ZoneId 成员的比较。所以我们看到 class(Java-8 中的源代码):

/**
 * Checks if this time-zone ID is equal to another time-zone ID.
 * <p>
 * The comparison is based on the ID.
 *
 * @param obj  the object to check, null returns false
 * @return true if this is equal to the other time-zone ID
 */
@Override
public boolean equals(Object obj) {
    if (this == obj) {
       return true;
    }
    if (obj instanceof ZoneId) {
        ZoneId other = (ZoneId) obj;
        return getId().equals(other.getId());
    }
    return false;
}

词法表示 "Etc/UTC" 和 "UTC" 显然是不同的字符串,因此 zoneId 比较平凡地产生 false。此行为在 javadoc 中有描述,因此我们没有错误。我强调 documentation:

的声明

The comparison is based on the ID.

也就是说,ZoneId 就像指向区域数据的命名指针,并不代表数据本身。

但我假设您更愿意比较不同区域 ID 的规则,而不是词汇表示。然后问规则:

ZoneId z1 = ZoneId.of("Etc/UTC");
ZoneId z2 = ZoneId.of("UTC");
System.out.println(z1.equals(z2)); // false
System.out.println(z1.getRules().equals(z2.getRules())); // true

因此您可以使用区域规则与 ZonedDateTime 的其他非区域相关成员的比较(有点尴尬)。

顺便说一句,我强烈建议不要使用 "Etc/..." 标识符,因为("Etc/GMT" 或 "Etc/UTC" 除外)它们的 偏移符号位于与通常预期的相反

另一个 关于 ZonedDateTime-实例 比较的重要说明。看这里:

System.out.println(zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc)); // -16
System.out.println(z1.getId().compareTo(z2.getId())); // -16

我们看到 ZonedDateTime-具有相同即时和本地时间戳的实例的比较是基于区域 ID 的词法比较。通常不是大多数用户所期望的。但这也不是错误,因为这种行为是 described in the API。这是我不喜欢使用 ZonedDateTime 类型的另一个原因。它本质上太复杂了。您应该只将它用于 Instant 和本地类型恕我直言之间的中间类型转换。这具体的意思是:在你比较ZonedDateTime-实例之前,请将它们转换(通常是Instant)然后比较。

2018-02-01 更新:

为此问题打开的 JDK-issue 已被 Oracle 关闭为 "Not an issue"。

您可以将 ZonedDateTime 对象转换为 Instant,正如另一个 answers/comments 已经说明的那样。

ZonedDateTime::isEqual

或者您可以使用 isEqual method,它比较两个 ZonedDateTime 实例是否对应于相同的 Instant:

ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime zoneDateTimeEtcUtc = now.withZoneSameInstant(ZoneId.of("Etc/UTC"));
ZonedDateTime zoneDateTimeUtc = now.withZoneSameInstant(ZoneId.of("UTC"));

Assert.assertTrue(zoneDateTimeEtcUtc.isEqual(zoneDateTimeUtc));