ZonedDateTime 比较:预期:[Etc/UTC],但原为:[UTC]

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

根据这个问题: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)-16ZoneDateTime

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

断言结果:

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

因此,问题在于实施中的某个地方。但我仍然期望,如果两个区域ID都有效并且都指定了相同的区域,那么它们应该是相等的。ZoneId

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

更新

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

    @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

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

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

更新-2我报告了一个错误,ID:9052414。将看到甲骨文团队将决定什么。

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


答案 1

您可以将对象转换为 ,如其他答案/注释已告知的那样。ZonedDateTimeInstant

ZonedDateTime::isEqual

或者,您可以使用 isEqual 方法,该方法比较两个实例是否对应相同的实例:ZonedDateTimeInstant

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

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

答案 2

该类在其 -方法中使用内部成员的比较。所以我们在那个类中看到(Java-8中的源代码):ZonedDateTimeequals()ZoneId

/**
 * 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比较平凡地产生。这种行为在javadoc中有所描述,所以我们没有bug。我强调文档的声明:false

比较基于 ID。

也就是说,a 类似于指向区域数据的命名指针,并不表示数据本身。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

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

2018-02-01更新:

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