Skip to content
academia.sh

Lesson 14 / 15

Date and Time API

Measured by running whether each of three types (instant, local date-time, zoned date-time) can correctly answer the question 'does this show the same instant.' It shows that comparing without a time zone silently gives the wrong result, and that the gap and the overlap at a daylight saving transition are a place where the API picks a side by rule.

Contents

The previous lesson measured what happens when an object is turned into bytes — the constructor never ran there, and time never entered the picture at all, because everything measured was an in-memory, fixed byte stream. One of the most common kinds of data a system carries is time itself, and time is represented by three separate types: Instant is an instant, LocalDateTime is a local date-time, ZonedDateTime is a zoned date-time. These three types do not carry the same information — one never knows the time zone, one always does — and this lesson measures where that difference matters.

The course’s question has not changed: which side holds a guarantee — here, the guarantee of correctly answering “do these two values show the same instant”? This time the answer is not a choice of class, it is a choice of type. Three sections proceed in order. The first tests all three types against the same question. The second measures a concrete caller mistake in which this question is answered wrongly. The third measures an edge case — a daylight saving transition — where even adding a time zone does not answer every question.

These three are another form of what the previous lesson measured. The serialization lesson measured what information a byte stream carries; the same question is asked here, except what is being represented is not an object but time. A LocalDateTime, just like a record whose field was renamed, silently produces a wrong result when used as though it carried information it does not have — the difference is that there a serialization version ID was missing, here a time zone is missing.

Determinism note: no real clock is ever read in this lesson. Every example is built on a fixed Instant or on a date parsed from a fixed string; the daylight saving transitions are also discovered programmatically from the system’s own time zone data, no date is asserted in the prose.

Three Types, One Question

  • IO22 — A single Instant is applied to two different time zones (Istanbul, New York), producing two ZonedDateTime values; these two values show the same instant but carry different local times. Four separate methods try to answer the same question (“are these two the same instant”): Instant.equals, LocalDateTime.equals, ZonedDateTime.equals, ZonedDateTime.isEqual.
// ThreeTypes.java — which of the three types correctly answers "does this show the same instant"
import java.time.*;

public class ThreeTypes {
    public static void main(String[] args) {
        Instant instant = Instant.parse("2026-06-15T13:00:00Z");

        ZonedDateTime istanbul = instant.atZone(ZoneId.of("Europe/Istanbul"));
        ZonedDateTime newYork = instant.atZone(ZoneId.of("America/New_York"));
        LocalDateTime istanbulLocal = istanbul.toLocalDateTime();
        LocalDateTime newYorkLocal = newYork.toLocalDateTime();

        System.out.println("istanbul local time: " + istanbulLocal);
        System.out.println("new york local time: " + newYorkLocal);
        System.out.println();

        System.out.printf("%-42s%s%n", "question: does it show the same instant", "answer");
        System.out.printf("%-42s%s%n", "Instant.equals",
                instant.equals(Instant.parse("2026-06-15T13:00:00Z")));
        System.out.printf("%-42s%s%n", "LocalDateTime.equals (local times)",
                istanbulLocal.equals(newYorkLocal));
        System.out.printf("%-42s%s%n", "ZonedDateTime.equals (zone+local+offset)",
                istanbul.equals(newYork));
        System.out.printf("%-42s%s%n", "ZonedDateTime.isEqual (instant only)",
                istanbul.isEqual(newYork));
    }
}
istanbul local time: 2026-06-15T16:00
new york local time: 2026-06-15T09:00

question: does it show the same instant   answer
Instant.equals                            true
LocalDateTime.equals (local times)        false
ZonedDateTime.equals (zone+local+offset)  false
ZonedDateTime.isEqual (instant only)      true

Both ZonedDateTime values were produced from the same Instant, meaning both show exactly the same instant — even though one reads 16:00 and the other 09:00. Two of the four methods get this right: Instant.equals (which is already comparing the same instant) and ZonedDateTime.isEqual (a method written specifically to compare only the instant a value represents). LocalDateTime.equals says false, but it is not answering this question wrongly — it cannot answer it at all, because the values it holds carry no information about which time zone they belong to; it only says whether two calendar readings are equal to each other. The most surprising line is the third: ZonedDateTime.equals says false even though it holds time zone information, because that method answers not “is it the same instant” but “are the zone, the local time, and the offset all exactly identical.” Two methods exist on the same type and they answer different questions; choosing the right one is left to the caller.

These four results follow directly from what the three types carry. Instant is a single point on a number line, and if two points are at the same place equals says so directly; it has no other question to ask. ZonedDateTime carries that same point together with a calendar reading as well — which zone, which local time — and equals folds this extra information into the comparison too; even if two representations show the same instant, equals counts them as different if their zones differ, because it defines the two values as identical representations, not as the same instant. isEqual deliberately ignores this extra information and looks only at the instant represented. LocalDateTime, on the other hand, never carries this extra information at all; it has no zone, no offset to compare, and so it answers not the question that was asked but the only question it can ask — whether two calendar readings are equal.

The Caller’s Rule: Comparing Without a Time Zone

  • IO23 — Two records carry the same local time value (09:00), but one was entered in Istanbul and one in New York; which time zone it was entered in is not kept together with the record. A comparison made without this information is run side by side with a comparison made with the information supplied.

This situation is not made up: a scheduling system, a log, a scheduled task — every system that takes time in the user’s own local time runs into the same choice. Storing the time zone together with the record is a design decision, the language itself does not impose it; LocalDateTime deliberately exists as a type with no time zone, because most of the time a single calendar reading really is enough. The cost only shows up once that reading is used as though it meant “the same instant.”

// MissingZone.java — comparing without a time zone silently gives the wrong result
import java.time.*;

public class MissingZone {
    static boolean naiveSameInstant(LocalDateTime a, LocalDateTime b) {
        return a.equals(b);
    }

    static boolean correctSameInstant(LocalDateTime a, ZoneId zoneA, LocalDateTime b, ZoneId zoneB) {
        return a.atZone(zoneA).toInstant().equals(b.atZone(zoneB).toInstant());
    }

    public static void main(String[] args) {
        LocalDateTime recordA = LocalDateTime.of(2026, 6, 15, 9, 0);
        LocalDateTime recordB = LocalDateTime.of(2026, 6, 15, 9, 0);
        ZoneId zoneA = ZoneId.of("Europe/Istanbul");
        ZoneId zoneB = ZoneId.of("America/New_York");

        System.out.println("record a (local): " + recordA + " (Istanbul)");
        System.out.println("record b (local): " + recordB + " (New York)");
        System.out.println("naive comparison (without a time zone): "
                + naiveSameInstant(recordA, recordB));
        System.out.println("correct comparison (with a time zone): "
                + correctSameInstant(recordA, zoneA, recordB, zoneB));

        Duration diff = Duration.between(recordA.atZone(zoneA).toInstant(),
                recordB.atZone(zoneB).toInstant());
        System.out.println("actual difference: " + diff.toHours() + " hours");
    }
}
record a (local): 2026-06-15T09:00 (Istanbul)
record b (local): 2026-06-15T09:00 (New York)
naive comparison (without a time zone): true
correct comparison (with a time zone): false
actual difference: 7 hours

The naive comparison says “same time” because the two LocalDateTime values really are equal — both read 09:00. But in reality there is a seven-hour difference between them. This is not a flaw in the library: LocalDateTime.equals already says it is comparing only two calendar readings and promises nothing else. The flaw is in using a type that never carries time zone information as though it did. The correct comparison attaches a time zone to both sides, converts to Instant, and only then compares — giving the right answer, false, and the seven-hour difference underneath it. This is the direct consequence of the previous section: LocalDateTime was the type that could not answer “is it the same instant”; here that same gap takes concrete shape as a record-comparison mistake.

This rule is never enforced by the compiler. naiveSameInstant is a fully valid, type-correct method that takes two LocalDateTime values and returns a boolean; the compiler cannot know that it gives the wrong answer to a semantically wrong question, because all it has is the fact that both parameters are the same class. The result takes the same shape as the flaw measured in the previous lesson: the mistake does not even appear as an exception at runtime, it just passes silently as a wrong boolean. The Duration.between call requires both sides to be explicitly converted to an Instant to make this difference visible — meaning the cost of writing the correct comparison is not forgetting the time zone, knowing all the while that the compiler will give no warning if it is forgotten.

Gap and Overlap at a Daylight Saving Transition

  • IO24 — The system’s New York time zone rules are queried for the first two transitions after a fixed starting instant. The first transition is a gap (a one-hour range of local time is never lived through), the second is an overlap (a one-hour range of local time is lived through twice). Which date this is is never asserted in the prose; the program uses whatever date it finds.

The previous two sections dealt with a time zone that was missing; this section measures where the boundary lies even when the time zone is complete. ZoneId.getRules() returns a ZoneRules object carrying all of a region’s transition information; this information is not a constant written into the code, it is read from the virtual machine’s own time zone data. nextTransition finds the first transition after a given instant — the result depends on which instant is supplied, but this dependency does not make the measurement indeterminate, because the starting instant here too is parsed from a fixed string.

// RuleDiscovery.java — daylight saving transitions are discovered from the system's own time zone data
import java.time.*;
import java.time.zone.*;

public class RuleDiscovery {
    public static void main(String[] args) {
        ZoneId zone = ZoneId.of("America/New_York");
        ZoneRules rules = zone.getRules();
        Instant start = Instant.parse("2020-01-01T00:00:00Z");

        ZoneOffsetTransition firstTransition = rules.nextTransition(start);
        ZoneOffsetTransition secondTransition = rules.nextTransition(firstTransition.getInstant());

        System.out.println("is the first transition a gap: " + firstTransition.isGap());
        System.out.println("first transition local time before/after: "
                + firstTransition.getDateTimeBefore() + " / " + firstTransition.getDateTimeAfter());
        System.out.println("is the second transition an overlap: " + secondTransition.isOverlap());
        System.out.println("second transition local time before/after: "
                + secondTransition.getDateTimeBefore() + " / " + secondTransition.getDateTimeAfter());

        LocalDateTime insideGap = firstTransition.getDateTimeBefore().plusMinutes(30);
        ZonedDateTime gapResult = insideGap.atZone(zone);
        System.out.println();
        System.out.println("local time inside the gap: " + insideGap);
        System.out.println("default resolution: " + gapResult);
        System.out.println("was the local time pushed forward: "
                + !gapResult.toLocalDateTime().equals(insideGap));

        LocalDateTime insideOverlap = secondTransition.getDateTimeAfter();
        ZonedDateTime earlyOffset = insideOverlap.atZone(zone);
        ZonedDateTime lateOffset = earlyOffset.withLaterOffsetAtOverlap();
        System.out.println();
        System.out.println("local time inside the overlap: " + insideOverlap);
        System.out.println("default resolution (early offset): " + earlyOffset);
        System.out.println("when the later offset is requested: " + lateOffset);
        System.out.println("same local time, same instant: "
                + earlyOffset.toLocalDateTime().equals(lateOffset.toLocalDateTime()) + ", "
                + earlyOffset.toInstant().equals(lateOffset.toInstant()));
    }
}
is the first transition a gap: true
first transition local time before/after: 2020-03-08T02:00 / 2020-03-08T03:00
is the second transition an overlap: true
second transition local time before/after: 2020-11-01T02:00 / 2020-11-01T01:00

local time inside the gap: 2020-03-08T02:30
default resolution: 2020-03-08T03:30-04:00[America/New_York]
was the local time pushed forward: true

local time inside the overlap: 2020-11-01T01:00
default resolution (early offset): 2020-11-01T01:00-04:00[America/New_York]
when the later offset is requested: 2020-11-01T01:00-05:00[America/New_York]
same local time, same instant: true, false

Scanning the system’s time zone rules forward from its own starting instant, the program finds two transitions: one is a gap, one is an overlap. In the gap, the clock jumps straight from 02:00 to 03:00 — a local time of 02:30 is never lived through in that region on that day. So when we attach 02:30 to that zone, the API does not accept it as given; it pushes it forward by the length of the gap, moving it to 03:30. The overlap works the opposite way: 01:00 is lived through twice that day, once under daylight time (offset −4), once under standard time (offset −5). The API has to make a choice, and by default it picks the earlier offset; when withLaterOffsetAtOverlap is called, the same local time is instead attached to the real instant one hour later — the local time does not change, but the instant it represents does.

Why the two cases turn out differently follows from the transition’s own definition. In a gap the clock genuinely jumps — after 02:00 the next real instant is read as 03:00 — so the only real instant that corresponds to the local times in between is where the jump ends; that is why the API pushes forward, not back. In an overlap the clock winds back: the same local reading appears at two different real instants, and a choice has to be made between them because both are equally valid. getDateTimeBefore and getDateTimeAfter, the two methods used in this measurement, say which direction the transition runs: in a gap the second value is greater than the first, in an overlap it is smaller — whether a transition is a gap or an overlap can be read off from this too, isGap/isOverlap only names it.

This section’s limiting measurement is this: adding a time zone solves the mistake from the previous section, but it does not answer every question. In the overlap case, even the local time that comes with a zone is not enough on its own — the same local time, in the same zone, can correspond to two different real instants, and the API itself has to choose which one is meant. This choice is not arbitrary: the system’s time zone rules define which local times do not exist and which exist twice, and the API picks one side according to that rule. The choice itself is fixed and documented; but it may not be the side the caller wanted, and the only way to notice is to use queries like isGap/isOverlap.

Summary

  • Instant is an instant and carries no time zone; LocalDateTime is a local date-time and carries no time zone; ZonedDateTime is a zoned date-time and does carry a time zone.
  • Instant.equals and ZonedDateTime.isEqual correctly answer “does it show the same instant”; LocalDateTime.equals cannot answer this question at all, and ZonedDateTime.equals answers a different question (are the zone, the local time, and the offset all exactly identical).
  • Two local date-times compared without a time zone can come out equal even when a real difference exists between them; the flaw is not in the library, it is in assuming information the type does not carry.
  • In the gap of a daylight saving transition, a range of local time is never lived through and the API pushes it forward by the length of the transition; in the overlap, a range of local time is lived through twice and the API picks the earlier offset by default.
  • The limiting measurement: adding a time zone does not answer every question. An overlapping local time can correspond to two different real instants even within the same zone, and which one is meant can only be understood with queries like isGap/isOverlap.
  • Gap and overlap come from the direction of the transition: when the clock jumps forward, the local times in between are never lived through; when the clock winds back, a range of local time is lived through twice.

Next Step

Everything measured in this lesson was numeric and calendar-based. But the standard library’s input-output layer also has a text-matching question: asking whether a string matches a given pattern. The next and final lesson of the course looks at regular expressions — showing that a pattern is a compiled value, that a greedy and a reluctant quantifier produce different captures on the same input, and that backtracking is measured by its number of steps.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close