What is the spring-forward gap in DST, and what happens in java.time when you create a ZonedDateTime at a local time that does not exist?
answer
- Spring-forward = clock jumps, an hour vanishes
- Gap local times do not exist as an instant
- java.time shifts forward by the gap, never throws
- Detect via ZoneRules.getTransition(...).isGap()
- 02:30 Berlin -> 03:30 +02:00
basics
~20 sIn spring, clocks jump forward (e.g. 02:00 to 03:00), so an hour of local time never happens. If you ask java.time for a ZonedDateTime in that missing hour, it pushes the time forward by the gap (the missing hour) instead of failing.
solid answer
~40 sDaylight Saving Time means clocks shift twice a year. In spring-forward, the clock skips an hour: at 02:00 it jumps to 03:00, so local times like 02:30 do not exist on that day. When you build a ZonedDateTime for a non-existent local time, java.time does not throw. By default it resolves the gap by shifting the instant forward by the length of the gap (usually one hour) and applying the later offset, so 02:30 becomes 03:30 in the new offset. This is documented behavior of ZonedDateTime.of and ofLocal: in a gap the local time is adjusted forward; in an overlap it picks the earlier offset. Knowing this prevents off-by-one-hour bugs when scheduling near the spring transition.
code
java · 7 linesZoneId berlin = ZoneId.of("Europe/Berlin");
LocalDateTime missing = LocalDateTime.parse("2024-03-31T02:30");
ZonedDateTime z = missing.atZone(berlin);
System.out.println(z); // 2024-03-31T03:30+02:00[Europe/Berlin]
var t = berlin.getRules().getTransition(missing);
System.out.println(t != null && t.isGap()); // truego deeper
Knows DST exists and that spring loses an hour; can state java.time shifts the time forward rather than throwing.
Can show the exact transformation (02:30 -> 03:30 with the later offset) and detect a gap with ZoneRules.getTransition.
Reasons about the scheduling consequences (a local-time job firing late on transition day) and chooses to reject/log vs accept the shift.
Sets org-wide policy: store instants/UTC, schedule in UTC where possible, and define how gap inputs are validated across services to avoid silent shifts.
**Background terms.** - **Local time / `LocalDateTime`**: a date-and-time with no time zone, e.g. `2024-03-31T02:30`. It is just "wall-clock" numbers; it does not by itself name a moment on the global timeline. - **Time zone / `ZoneId`**: a named region (e.g. `Europe/Berlin`) whose rules say which UTC **offset** applies at any moment. - **Offset / `ZoneOffset`**: the fixed difference from UTC, e.g. `+01:00` (winter) or `+02:00` (summer). - **Instant**: an exact point on the global timeline (UTC seconds since 1970). A `ZonedDateTime` = local time + zone, which together resolve to one instant. - **DST (Daylight Saving Time)**: many zones move their clocks forward in spring and back in autumn to shift daylight. The forward move is **spring-forward**; the backward move is **fall-back**. **The spring-forward gap.** When a zone springs forward, say at `02:00` the clock instantly becomes `03:00`, the wall-clock times `02:00:00`–`02:59:59` **never occur** that day. This missing interval is the **gap** (typically one hour). A `LocalDateTime` inside the gap is a perfectly valid date-time object, but it corresponds to **no real instant** in that zone. **What java.time does.** `ZonedDateTime.of(localDateTime, zone)` (and `LocalDateTime.atZone(zone)`, which calls the same logic) must turn the local time into an instant. The contract for a gap is: **shift the local time forward by the size of the gap and use the offset after the transition**. So `2024-03-31T02:30` in `Europe/Berlin` becomes `2024-03-31T03:30+02:00`, not an exception, not a null. The instant produced is the moment one hour later than the naive reading would suggest. The rules come from `ZoneRules.getTransition(localDateTime)`, which returns a `ZoneOffsetTransition`; for a gap, `isGap()` is true and the resolution adds `transition.getDuration()`. **Why it never throws.** java.time deliberately favors a total, predictable function over exceptions: every `LocalDateTime` maps to exactly one `ZonedDateTime`. The cost is that a silently-shifted value can surprise you, hence the bugs around scheduling. **How to detect a gap yourself.** Use `zone.getRules().getTransition(local)`; if it returns a non-null transition with `isGap() == true`, your local time was invalid and got moved. You can then decide to reject the input, log it, or accept the shift. **Practical impact.** A daily 02:30 job scheduled by local time will, on the spring-forward day, actually fire at 03:30 (the gap pushed it). Alarm/cron-style code that assumes "02:30 exists every day" is wrong twice a year for affected zones.
- How can you tell, in code, that a given LocalDateTime fell into a spring-forward gap?Call zone.getRules().getTransition(localDateTime). If it returns a non-null ZoneOffsetTransition whose isGap() is true, the local time does not exist and resolution shifts it forward by getDuration().
- Does this gap behavior affect Instant or epoch-millis arithmetic?No. Instant and epoch time are pure UTC points with no DST. The gap only matters when converting a wall-clock LocalDateTime to/from a zoned time.
saying these in an interview costs you the question
- Claiming java.time throws an exception for a gap time (it does not by default)
- Thinking a gap LocalDateTime is invalid to construct (only its zone resolution is special)
- Assuming the gap is always exactly one hour (most are, but the duration comes from the rules)
- Believing UTC or Instant are affected by DST (they are not; only wall-clock zones)