17

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

根据这个问题:UTC 和 Etc/UTC 时区之间有区别吗?- 这两个区域是相同的。但是我的测试失败了:

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)返回-16值,因为内部的这个实现ZoneDateTime

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

断言结果:

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

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

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

更新

有几个人试图说服我这是一种正常行为,比较方法的实现String使用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/UTC 相同UTC,那么我看到两个选项:

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

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

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

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

4

3 回答 3

11

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

ZonedDateTime::isEqual

或者您可以使用该isEqual方法比较两个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));
于 2018-01-29T17:32:38.260 回答
3

该类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 中进行了描述,因此我们没有错误。我强调文档的声明:

比较基于 ID。

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

但我假设您更愿意比较两个不同区域 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

所以你可以使用zone规则和其他非zone相关成员的比较ZonedDateTime(有点尴尬)。

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

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

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

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

2018-02-01 更新:

为这个问题打开的JDK-issue已被 Oracle 关闭为“不是问题”。

于 2018-01-26T14:50:23.167 回答
2

ZoneDateTime如果要比较时间,应该转换为OffsetDateTime然后比较。compareTo(..)

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

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

该方法ZoneId.of(..)正在解析您提供的字符串并在需要时对其进行转换。ZoneId表示时区而不是偏移量,即与 GMT 时区的时移。WhileZoneOffset表示偏移量。 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 兼容。

soUTC+0被转换为UTCwhile Etc/UTC 保持不变

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

于 2018-01-26T14:24:12.880 回答