【问题标题】:ZonedDateTime comparison: expected: [Etc/UTC] but was: [UTC]ZonedDateTime 比较:预期:[Etc/UTC] 但原为:[UTC]
【发布时间】:2018-07-05 21:01:14
【问题描述】:

我正在比较两个似乎相等的日期,但它们包含不同的区域名称:一个是 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 suggested,我可能应该使用compareTo(...) 方法。这是个好主意,但是zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc) 返回-16 值,因为ZoneDateTime 内部的这个实现:

cmp = getZone().getId().compareTo(other.getZone().getId());

断言结果:

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

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

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

更新

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

    @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/UTC UTC 相同,那么我看到两个选项:

  • compareTo/equals 方法不应使用 ZoneId id,而应比较它们的规则
  • Zone.of(...) 已损坏,应将 Etc/UTCUTC 视为相同的时区。

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

UPDATE-2我报告了一个错误,ID : 9052414。将看看 Oracle 团队将做出什么决定。

UPDATE-3 已接受错误报告(不知道他们是否会将其关闭为“无法修复”):https://bugs.openjdk.java.net/browse/JDK-8196398

【问题讨论】:

  • 你喜欢搜索这些错误吗? :)
  • 说真的,我今天没喝酒。我在工作中发现了它:)
  • 可能只是一个提示,我现在看不到 :(,但是如果 equals 也从区域中查看实际的字符串会怎样
  • @Eugene 当然可以!问题是这个区域不相等
  • @Eugene,我已经更新了我的问题,将问题缩小到ZoneId

标签: java date datetime zoneddatetime datetime-comparison


【解决方案1】:

您可以将 ZonedDateTime 对象转换为 Instant,正如其他答案/cmets 已经告知的那样。

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));

【讨论】:

  • isEqual 真是个好观察!您的回答没有为我的问题提供确切的答案,但我赞成这个好主意。
【解决方案2】:

ZonedDateTime 类在其equals()-方法中使用内部ZoneId-成员的比较。所以我们在那个类中看到(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-comparison 很容易产生false。此行为在 javadoc 中进行了描述,因此我们没有错误。我强调documentation的声明:

比较基于 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-instances 比较的重要说明。看这里:

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

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

2018 年 2 月 1 日更新:

针对此问题打开的 JDK-issue 已被 Oracle 关闭为“不是问题”。

【讨论】:

  • 现在这就是我在 cmets 中的意思,必须有一种方法来实际比较它们...... 1+
  • 感谢您的分析。但是我们都可以阅读源代码和阅读 javadocs。 :-) 说真的,不要被冒犯 :-) 。这并不能解释为什么zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc) 不等于0。如果区域相等,那么这些区域的日期时间表示也应该相等。这就是为什么我认为这是实施中的一个错误
  • @Eugene 比较 Zones 的规则并不能解决比较用它们构造的两个 ZonedDateTime 的初始问题...
  • 您可以执行zdt1.toInstant().equals(zdt2.toInstant()) &amp;&amp; zdt1.getZone().getRules().equals(zdt2.getZone().getRules())zdt1.withFixedOffsetZone().equals(zdt2.withFixedOffsetZone()) 之类的操作。它不是很优雅,但很有效。
  • @Andremoniy ZonedDateTime 实现Comparable 接口的事实需要问题。您希望比较对ZoneId 进行基于规则的比较是可以理解的,但忽略了a)还有另一种非时间状态,如Chronology 和b)比较可以在反序列化的上下文中中断(很难理解乍一看)。我个人认为ZonedDateTime 有多么有用,可以在here 找到ZonedDateTime 类型在许多微妙的方面都太复杂了。应该首选 Instant 和本地类型。
【解决方案3】:

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-

如果区域 ID 等于“GMT”、“UTC”或“UT”,则结果是具有相同 ID 和规则的 ZoneId,等效于 ZoneOffset.UTC。

如果区域 ID 以“UTC+”、“UTC-”、“GMT+”、“GMT-”、“UT+”或“UT-”开头,则该 ID 是基于偏移量的前缀 ID。 ID 分为两部分,前缀为两个或三个字母,后缀以符号开头。后缀被解析为 ZoneOffset。结果将是具有指定 UTC/GMT/UT 前缀的 ZoneId 和按照 ZoneOffset.getId() 的规范化偏移 ID。返回的 ZoneId 的规则将等同于解析后的 ZoneOffset。

所有其他 ID 都被解析为基于区域的区域 ID。区域 ID 必须与正则表达式 [A-Za-z][A-Za-z0-9~/._+-]+ 匹配,否则会引发 DateTimeException。如果区域 ID 不在配置的 ID 集中,则抛出 ZoneRulesException。区域 ID 的详细格式取决于提供数据的组。默认数据集由 IANA 时区数据库 (TZDB) 提供。这具有“{area}/{city}”形式的区域 ID,例如“Europe/Paris”或“America/New_York”。这与 TimeZone 中的大多数 ID 兼容。

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

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

【讨论】:

  • zoneDateTimeEtcUtc.compareTo(zoneDateTimeUtc) 返回-16
  • @Andremoniy 很好的观察。比较最终委托给ZoneId.getId()-comparison,请参阅我的答案。
  • 顺便说一句,@Nicolas,javadoc 中也说:`它是“与等式一致”,由 Comparable 定义。所以 equals 也是比较这些对象的正确方法
  • @Andremoniy 声明“与等号一致”并不一定意味着它是纯粹的时间比较。实际上,这里考虑了其他非时间状态,例如 ZoneId 的词汇表示。正如我在回答中所说:ZoneId 在语义上只是一个名称引用规则/数据的指针,但不代表规则本身。 Oracle 永远不会接受这是一个错误,因为他们甚至在官方 API 中详细描述了这种奇怪的行为。没有机会在 Java 中改变它。恕我直言,设计错误使ZonedDateTime 完全具有可比性
  • @MenoHochschild 恕我直言,让 ZonedDateTime 完全具有可比性的设计错误 - 我同意!!!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-20
  • 1970-01-01
  • 1970-01-01
  • 2016-07-26
  • 1970-01-01
  • 1970-01-01
  • 2020-09-30
相关资源
最近更新 更多