Precautions when using ZonedDateTime - Object.equals vs Assertions.isEqualTo
Overview
In Java, there are several objects that can represent time. In this article, we will discuss how time comparison is done with ZonedDateTime, which is one of the objects that contains the most information.
Different but the same time?
Let's write a simple test code to find any peculiarities.
ZonedDateTime seoulZonedTime = ZonedDateTime.parse("2021-10-10T10:00:00+09:00[Asia/Seoul]");
ZonedDateTime utcTime = ZonedDateTime.parse("2021-10-10T01:00:00Z[UTC]");
assertThat(seoulZonedTime.equals(utcTime)).isFalse();
assertThat(seoulZonedTime).isEqualTo(utcTime);
This code passes the test. Although equals returns false, isEqualTo passes. Why is that?
In reality, the two ZonedDateTime objects in the above code represent the same time. However, since ZonedDateTime internally contains LocalDateTime, ZoneOffset, and ZoneId, when compared using equals, it checks if the objects have the same values rather than an absolute time.
Therefore, equals returns false.
ZonedDateTime#equals
However, it seems that isEqualTo works differently in terms of how it operates in time objects.
In fact, when comparing ZonedDateTime, isEqualTo calls ChronoZonedDateTimeByInstantComparator#compare instead of invoking ZonedDateTime's equals.

Comparator#compare is called.
By looking at the internal implementation, it can be seen that the comparison is done by converting to seconds using toEpochSecond(). This means that it compares absolute time through compare rather than comparing objects through equals.
Based on this, the comparison of ZonedDateTime can be summarized as follows:
equals
: Compares objects
isEqualTo
: Compares absolute time
Therefore, when comparing objects that include ZonedDateTime indirectly, equals is called, so if you want to compare based on the absolute value of ZonedDateTime, you need to override the equals method inside the object.
public record Event(
String name,
ZonedDateTime eventDateTime
) {
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Event event = (Event) o;
return Objects.equals(name, event.name)
&& Objects.equals(eventDateTime.toEpochSecond(), event.eventDateTime.toEpochSecond());
}
@Override
public int hashCode() {
return Objects.hash(name, eventDateTime.toEpochSecond());
}
}
@Test
void equals() {
ZonedDateTime time1 = ZonedDateTime.parse("2021-10-10T10:00:00+09:00[Asia/Seoul]");
ZonedDateTime time2 = ZonedDateTime.parse("2021-10-10T01:00:00Z[UTC]");
Event event1 = new Event("event", time1);
Event event2 = new Event("event", time2);
assertThat(event1).isEqualTo(event2); // pass
}
Conclusion
- If you want to compare absolute time when
equalsis called betweenZonedDateTime, you need to convert it, such as usingtoEpochSecond(). - When directly comparing
ZonedDateTimewithisEqualToin test code or similar scenarios,equalsis not called, and internal conversion is performed, so no separate conversion is needed. - If there is a
ZonedDateTimeinside an object, you may need to override the object'sequalsmethod as needed.
