Java Date-Time API: Dates, Durations, Periods and Time Zones (OCP Java SE 25)
The Java Date-Time API (<code>java.time</code>) is the standard library of immutable classes for dates, times, instants, time zones and amounts of time: <code>LocalDate</code>, <code>LocalTime</code>, <code>LocalDateTime</code>, <code>Instant</code>, <code>ZonedDateTime</code>, <code>OffsetDateTime</code>, <code>Duration</code> and <code>Period</code>. Objective 1.3 of the Java SE 25 Developer exam asks you to manipulate all of them, including across daylight saving time. This lesson covers how values are created and validated, why every operation returns a new object, how months clamp and times wrap, the difference between <code>Duration</code> and <code>Period</code>, how <code>between()</code> truncates, which units each type supports, and how zones, offsets, DST gaps and overlaps change the result of date-based and time-based arithmetic.
On this page10 sections
- The java.time types: which class models what
- Creating values: of() factories validate at run time
- Immutable arithmetic: discarded results, month ends and midnight wraparound
- Duration versus Period: what each stores and how it prints
- Measuring between two values: between() and until() truncate
- Which units and amounts each type supports
- Time zones: ZoneId, ZoneOffset and converting between zones
- Daylight saving gaps and overlaps
- Date-based versus time-based arithmetic across DST
- TemporalAdjusters: next, nextOrSame, first and last
- Choose the right java.time class for a date, a time, an instant, a zoned or offset date-time, and an amount of time.
- Predict when of() factories and with methods throw DateTimeException, and when plusMonths/withMonth clamp the day instead.
- Trace immutable arithmetic, including discarded results, month-end clamping and LocalTime wraparound past midnight.
- Read and produce the ISO-8601 strings of Duration and Period, and distinguish total (toX) from component (toXPart) accessors.
- Calculate differences with Period.between, ChronoUnit.between, until and Duration.between, including truncation and negative results.
- Convert between time zones with withZoneSameInstant and withZoneSameLocal, and compare zoned values with equals versus isEqual.
- Resolve daylight saving gaps and overlaps and explain why date-based and time-based additions differ across a DST change.
The java.time types: which class models what
The Date-Time API (java.time) gives each kind of value its own immutable class, and choosing the right one decides which operations are legal. The local types have no zone at all, Instant is a point on the UTC timeline, and the zoned and offset types tie a local value to a place on that timeline.
| Type | Holds | Example toString() |
|---|---|---|
LocalDate | year-month-day, no time, no zone | 2026-08-21 |
LocalTime | time of day to the nanosecond, no date | 14:20, 14:20:06 |
LocalDateTime | date + time, no zone | 2026-08-21T14:20 |
Instant | a moment on the UTC timeline (epoch seconds + nanos) | 2026-08-21T12:20:00Z |
OffsetDateTime | local date-time + a fixed ZoneOffset | 2026-08-21T14:20+02:00 |
ZonedDateTime | local date-time + offset + a ZoneId with its rules | 2026-08-21T14:20+02:00[Europe/Madrid] |
Duration | an amount of time in seconds + nanos | PT1H35M |
Period | an amount of years, months and days | P2Y4M9D |
Two printing details are worth knowing exactly. LocalTime (and the time part of the local and zoned types) omits seconds when both seconds and nanoseconds are zero, so LocalTime.of(14, 20, 0) prints 14:20. Instant.toString() always prints seconds and ends in Z: Instant.ofEpochSecond(0) is 1970-01-01T00:00:00Z. Formatting with patterns (DateTimeFormatter, locales) belongs to the localization objective and is covered there.
Creating values: of() factories validate at run time
The of() factories check every field when the program runs and throw DateTimeException for an impossible value; the compiler never checks them. Months are 1-based (January is 1, or Month.JANUARY), hours run 0–23, minutes and seconds 0–59, and February 29 (or day 366 of a year) exists only in leap years.
LocalDate.of(2028, 2, 29); // ok: 2028 is a leap year
LocalDate.ofYearDay(2026, 365); // ok: 2026-12-31
LocalDate.of(2027, Month.FEBRUARY, 29);
// DateTimeException: Invalid date 'February 29' as '2027'
// is not a leap year
LocalDate.ofYearDay(2026, 366);
// DateTimeException: Invalid date 'DayOfYear 366' as '2026'
// is not a leap year
LocalDate.of(2026, 13, 1);
// DateTimeException: Invalid value for MonthOfYear
// (valid values 1 - 12): 13
LocalTime.of(10, 60);
// DateTimeException: Invalid value for MinuteOfHour
// (valid values 0 - 59): 60The same range rule applies to every field: an hour of 24 is rejected just as a month of 0 or 13 is, because there is no 24:00 in LocalTime — midnight is 00:00.
Other common entry points: now() on each type (reads the system clock, so its value is never a fixed answer), parse() for ISO strings such as Instant.parse("2026-04-17T08:45:00Z"), Instant.ofEpochSecond() / getEpochSecond(), the constants LocalTime.MIDNIGHT (00:00), NOON and MAX (23:59:59.999999999), and conversions such as atTime(), atZone(), toLocalDate() and toInstant(). There is no public constructor on any of these classes.
Immutable arithmetic: discarded results, month ends and midnight wraparound
Every java.time value is immutable, so plusX, minusX and withX never change the object they are called on — they return a new one, and a call whose result is not assigned has no effect. A call such as t.withMinute(45) on its own line is not a setter: t keeps its old minute.
LocalDate d = LocalDate.of(2026, Month.MAY, 18);
d.minusWeeks(2); // result discarded
LocalDate e = d.minusWeeks(2);
System.out.println(d + " " + e);
// 2026-05-18 2026-05-04Calls chain because each returns a new value: LocalTime.of(10, 15).plusHours(2).plusMinutes(50) applies both steps in order and gives 13:05. The amount classes behave the same way — Duration and Period are immutable too.
Adding months, or changing the month or year with withMonth()/withYear(), clamps the day to the last valid day of the new month instead of failing. Setting the day itself with withDayOfMonth() (or withDayOfYear()) validates and throws for a day that month does not have. Neither ever rolls over into the next month.
LocalDate aug31 = LocalDate.of(2026, 8, 31);
aug31.plusMonths(1); // 2026-09-30 (clamped)
aug31.plusMonths(1).plusMonths(1); // 2026-10-30
aug31.plusMonths(2); // 2026-10-31
aug31.withMonth(2); // 2026-02-28 (clamped)
aug31.withMonth(6).withDayOfMonth(31);
// DateTimeException: Invalid date 'JUNE 31'Because clamping happens at each step, two single-month additions can end on a different day from one two-month addition: the first step lost the 31st, and the second step has no memory of it. In a leap year a clamp into February lands on the 29th instead of the 28th. plusDays() and plusWeeks() simply count days across month and year boundaries.
LocalTime has no date, so its arithmetic wraps around midnight silently: LocalTime.of(21, 30).plusHours(4) is 01:30, LocalTime.of(23, 30).plusMinutes(30) equals LocalTime.MIDNIGHT, and LocalTime.MIDNIGHT.minusSeconds(1) is 23:59:59 — not LocalTime.MAX, which carries 999,999,999 nanoseconds. A wrapped result can be earlier than where it started, so isAfter() on it may be false.
Duration versus Period: what each stores and how it prints
Duration is an exact amount of time stored as seconds plus nanoseconds; Period is a calendar amount stored as separate years, months and days. Both print in ISO-8601 form, and the formats differ.
Duration | Period | |
|---|---|---|
| Stores | seconds + nanos | years, months, days (three ints) |
| Format | PTnHnMnS — never a day part | PnYnMnD — never weeks or time |
| Examples | ofMinutes(95) → PT1H35M; ofDays(3) → PT72H; ofSeconds(-150) → PT-2M-30S | ofWeeks(2) → P14D; of(2, 15, 45) → P2Y15M45D |
| Zero | PT0S | P0D |
| Meaning of "a day" | exactly 24 hours | one calendar day (23, 24 or 25 hours in a DST zone) |
Duration.toString() normalizes seconds into hours and minutes, and a negative duration puts the sign on each part (PT-2H-15M). Period is not normalized: it keeps what you gave it until you call normalized(), which folds months into years only — days are never converted, because months have no fixed length.
Period p = Period.of(2, 15, 45);
System.out.println(p + " " + p.normalized());
// P2Y15M45D P3Y3M45DA classic trap: Period.ofYears(), ofMonths(), ofDays() and ofWeeks() are static factories. Calling one on the result of another compiles (a static call through an instance expression is legal) but ignores that instance, so Period.ofDays(10).ofWeeks(1) is just P7D — the ten days are gone. To build a mixed period use Period.of(y, m, d) or the instance methods withMonths()/plusDays(): Period.ofYears(2).withMonths(5).plusDays(7) is P2Y5M7D.
Duration has two accessor families. toHours(), toMinutes() and getSeconds() return the total length in that unit; toHoursPart(), toMinutesPart() and toSecondsPart() return the component as toString() shows it, and keep the sign.
Duration d = Duration.ofSeconds(5432); // PT1H30M32S
d.toMinutes(); // 90 (total)
d.toMinutesPart(); // 30 (component)
d.toSecondsPart(); // 32 (component)
d.getSeconds(); // 5432 (total)
Duration n = Duration.ofMinutes(-135); // PT-2H-15M
n.toHoursPart(); // -2
n.toMinutesPart(); // -15Measuring between two values: between() and until() truncate
Every "how long between" calculation counts only complete units and drops the remainder; the result is negative when the second argument is earlier than the first. There are three tools:
Period.between(start, end)(orstart.until(end)on aLocalDate) returns aPeriodsplit into years, months and days.ChronoUnit.X.between(start, end)(orstart.until(end, unit)) returns alongcount of one unit.Duration.between(start, end)returns an exact time amount; it measures withstart.until(end, …)in nanoseconds or seconds, so the start type must support time units (aLocalDatedoes not).
LocalDate a = LocalDate.of(2023, 9, 15);
LocalDate b = LocalDate.of(2025, 4, 3);
Period p = Period.between(a, b); // P1Y6M19D
p.getMonths(); // 6 (month component)
p.toTotalMonths(); // 18 (years*12 + months)
ChronoUnit.MONTHS.between(a, b); // 18
ChronoUnit.MONTHS.between(b, a); // -18
ChronoUnit.YEARS.between(a, b); // 1
LocalDate c = LocalDate.of(2026, 3, 31);
LocalDate e = LocalDate.of(2026, 4, 30);
ChronoUnit.MONTHS.between(c, e); // 0: day 30 < day 31
ChronoUnit.DAYS.between(c, e); // 30
c.until(e); // P30DA month counts as complete only when the end day-of-month has reached the start day-of-month, which is why 31 March to 30 April is zero months even though April has no 31st. Multiplying a truncated YEARS count by 12 is not the same as counting months.
LocalTime differences assume both times are on the same day; they never wrap. Duration.between(LocalTime.of(21, 0), LocalTime.of(3, 0)) is PT-18H (negative, so isNegative() is true), and LocalTime.of(20, 0).until(LocalTime.of(1, 0), ChronoUnit.HOURS) is -19, not 5.
Which units and amounts each type supports
A type accepts only the units its fields can represent: LocalDate supports days and longer, LocalTime supports days' worth of time units only (nanos to half-days), and Instant supports nanos up to DAYS (a fixed 24 hours) but not months or years. The date-time types support both.
| Type | Time units (HOURS, Duration) | Date units (DAYS+, Period) |
|---|---|---|
LocalDate | No | Yes |
LocalTime | Yes | No |
LocalDateTime, ZonedDateTime, OffsetDateTime | Yes | Yes |
Instant | Yes | DAYS only — no WEEKS, MONTHS, YEARS |
These mismatches are not compile errors. plus(TemporalAmount), plus(long, TemporalUnit) and Duration.between(Temporal, Temporal) all take interfaces, so the code compiles and fails at run time with UnsupportedTemporalTypeException (a DateTimeException):
LocalDate.of(2026, 6, 1).plus(Duration.ofHours(48));
// UnsupportedTemporalTypeException: Unsupported unit: Seconds
LocalTime.of(8, 0).plus(Period.ofWeeks(1));
// UnsupportedTemporalTypeException: Unsupported unit: Days
Instant.EPOCH.plus(Period.ofYears(1));
// UnsupportedTemporalTypeException: Unsupported unit: Years
Duration.between(LocalDate.of(2026, 6, 1),
LocalDate.of(2026, 6, 8));
// UnsupportedTemporalTypeException: Unsupported unit: Seconds
LocalDateTime.of(2026, 6, 1, 9, 0).plus(Duration.ofHours(48));
// fine: 2026-06-03T09:00
Instant.EPOCH.plus(3, ChronoUnit.DAYS);
// fine: 1970-01-04T00:00:00Z
LocalDate.of(2026, 6, 1).plus(Period.ofMonths(1));
// fine: 2026-07-01The unit named in the message is the one the amount is stored in: a Duration is applied as seconds and nanos, which is why adding Duration.ofHours(48) to a LocalDate complains about Seconds, even though 48 hours is a whole number of days. A Period applies only its non-zero parts, so LocalTime.of(8, 0).plus(Period.ofMonths(0)) is simply 08:00: a zero period touches no field.
Time zones: ZoneId, ZoneOffset and converting between zones
A ZoneOffset is a fixed difference from UTC (such as -06:00); a region ZoneId such as America/Chicago names a set of rules that say which offset applies on each date, including daylight saving time. OffsetDateTime keeps its offset forever; ZonedDateTime recomputes the offset from the zone rules whenever it moves to a new local date-time.
- Offset-style IDs —
ZoneOffset.ofHoursMinutes(5, 30),ZoneId.of("UTC-03:00"),ZoneId.ofOffset("GMT", ...)— never change:getRules().isFixedOffset()is true. - Region IDs carry rules. Many have DST (
Europe/Paris,America/Chicago, andAustralia/Melbourne, whose summer falls in the northern winter); others, such asAsia/Kolkata, keep one offset all year — yet they are still region IDs, soisFixedOffset()is false for them.
To move a ZonedDateTime into another zone there are two methods with opposite meanings:
| Method | Keeps | Changes | Los Angeles 15:00 (-07:00) → Chicago |
|---|---|---|---|
withZoneSameInstant(zone) | the instant | the wall-clock time | 17:00-05:00 (same moment) |
withZoneSameLocal(zone) | the wall-clock time | the instant | 15:00-05:00 (two hours earlier) |
To convert by hand, go through UTC: subtract the source offset to get UTC, then add the target offset in force at that instant. 18:30 in Kolkata (+05:30) on 5 October 2026 is 13:00Z; nine hours later is 22:00Z, which is 23:00 in London while it is still on BST (+01:00). In code that is dep.plus(Duration.ofHours(9)).withZoneSameInstant(london). toInstant() performs the first half of the step: ZonedDateTime.of(2026, 1, 20, 5, 30, 0, 0, ZoneId.of("Asia/Kolkata")).toInstant() is 2026-01-20T00:00:00Z.
Comparing zoned values: equals() versus isEqual(). ZonedDateTime.equals() is true only when the local date-time, the offset and the zone are all equal; isEqual(), isBefore() and isAfter() compare the instant only. The same moment shown in two zones is therefore isEqual but not equals.
ZonedDateTime la = ZonedDateTime.of(
2026, 2, 1, 9, 0, 0, 0, ZoneId.of("America/Los_Angeles"));
ZonedDateTime chi =
la.withZoneSameInstant(ZoneId.of("America/Chicago"));
// chi is 2026-02-01T11:00-06:00[America/Chicago]
la.equals(chi); // false
la.isEqual(chi); // true
la.toInstant().equals(chi.toInstant()); // trueConverting to a local type throws the zone away, so comparisons can flip. 05:00 in Kolkata (+05:30) on 1 February is 23:30Z on 31 January, which is before 00:00 in London (+00:00) on 1 February, i.e. 00:00Z. Their LocalDateTimes, however, compare 05:00 with 00:00 on the same date, and 05:00 is later. OffsetDateTime follows the same pattern: equals() compares local value and offset, isEqual() the instant.
Daylight saving gaps and overlaps
When a zone moves its clocks, some local times happen twice and some never happen; ZonedDateTime resolves both cases without throwing.
Gap (spring forward). A local time that falls in the gap is moved forward by the length of the gap and given the later (summer) offset. Chicago skipped 02:00–03:00 on 8 March 2026, so 02:15 becomes 03:15-05:00. Paris skipped 02:00–03:00 on 29 March 2026, so 02:40 becomes 03:40+02:00. The same rule applies when the wall-clock result of an addition (for example plusDays()) lands in a gap. Use ZonedDateTime.ofStrict(ldt, offset, zone) when you want an invalid combination rejected — it throws DateTimeException ("does not exist in zone … due to a gap") instead.
Overlap (fall back). A local time that occurs twice keeps the earlier offset (the summer one, i.e. the first occurrence) by default. withLaterOffsetAtOverlap() selects the second occurrence, and withEarlierOffsetAtOverlap() goes back; outside an overlap both return the same value unchanged.
ZoneId chi = ZoneId.of("America/Chicago");
// clocks went back 02:00 -> 01:00 on 1 November 2026
ZonedDateTime first = ZonedDateTime.of(
LocalDateTime.of(2026, 11, 1, 1, 20), chi);
ZonedDateTime second = first.withLaterOffsetAtOverlap();
System.out.println(first.getOffset() + " "
+ second.getOffset() + " "
+ Duration.between(first, second));
// -05:00 -06:00 PT1H
ZonedDateTime start = ZonedDateTime.of(
2026, 11, 1, 0, 20, 0, 0, chi);
start.plusHours(1); // 2026-11-01T01:20-05:00
start.plusHours(2); // 2026-11-01T01:20-06:00The last two lines show why the wall clock is not a reliable guide on a transition day: two real hours after 00:20 the clock reads 01:20 again, now at the winter offset.
Date-based versus time-based arithmetic across DST
On a ZonedDateTime, adding a date-based amount (plusDays(), plusWeeks(), plusMonths(), a Period, or ChronoUnit.DAYS and longer) works on the local date-time and keeps the wall-clock time, then re-resolves the offset. Adding a time-based amount (plusHours(), plusMinutes(), or any Duration — including Duration.ofDays(1)) works on the instant and adds exact elapsed time.
ZoneId chi = ZoneId.of("America/Chicago");
// clocks went back 02:00 -> 01:00 on 1 November 2026
ZonedDateTime s = ZonedDateTime.of(2026, 10, 31, 9, 0, 0, 0, chi);
s.plusDays(1); // 2026-11-01T09:00-06:00
s.plus(Period.ofDays(1)); // 2026-11-01T09:00-06:00
s.plus(1, ChronoUnit.DAYS); // 2026-11-01T09:00-06:00
s.plusHours(24); // 2026-11-01T08:00-06:00
s.plus(Duration.ofDays(1)); // 2026-11-01T08:00-06:00Across a fall-back night the calendar day is 25 hours long, so 24 elapsed hours stop one hour short of the same wall-clock time; across a spring-forward night the day is 23 hours, so 24 elapsed hours land one hour past it. So Duration.ofDays(1) and ChronoUnit.DAYS sound alike but behave differently: the first is always 24 hours, the second is one calendar day. If a gap swallows the wall-clock result of a date-based addition, the gap rule above moves it forward.
The same split applies to measuring. Duration.between() and ChronoUnit.HOURS.between() on zoned values measure elapsed time, so a spring-forward day is 23 hours and a fall-back day 25 hours, while Period.between() on the dates still counts one day (P1D). On LocalDateTime there is no zone, so every day is 24 hours. A fixed-offset OffsetDateTime never changes offset, so adding a day to 2026-03-07T09:00-06:00 stays at -06:00 while the Chicago ZonedDateTime for the same moment moves to -05:00.
ZoneId paris = ZoneId.of("Europe/Paris");
// spring forward 29 March 2026, fall back 25 October 2026
ZonedDateTime m = ZonedDateTime.of(2026, 3, 29, 0, 0, 0, 0, paris);
Duration.between(m, m.plusDays(1)); // PT23H
LocalDateTime a = LocalDateTime.of(2026, 10, 24, 12, 0);
ChronoUnit.HOURS.between(a, a.plusDays(1)); // 24
ChronoUnit.HOURS.between(a.atZone(paris),
a.plusDays(1).atZone(paris)); // 25TemporalAdjusters: next, nextOrSame, first and last
A TemporalAdjuster passed to with() computes a related date, such as the next Friday or the last day of the month, and like every with call it returns a new object. The factory methods live in java.time.temporal.TemporalAdjusters.
LocalDate d = LocalDate.of(2026, 7, 9); // a Thursday
d.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));
// 2026-07-16: strictly after d
d.with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY));
// 2026-07-09: d itself qualifies
d.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
// 2026-07-06
d.with(TemporalAdjusters.firstDayOfMonth()); // 2026-07-01
d.with(TemporalAdjusters.lastDayOfMonth()); // 2026-07-31
d.with(TemporalAdjusters.firstDayOfNextMonth()); // 2026-08-01
d.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY));
// 2026-07-27
d.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.SATURDAY));
// 2026-07-11: the second SaturdayThe OrSame variants (nextOrSame, previousOrSame) return the date unchanged when it already falls on the requested day; the plain versions always move at least one day. lastDayOfMonth() knows each month's length and leap years (LocalDate.of(2028, 2, 10).with(lastDayOfMonth()) is 2028-02-29), so it is the safe way to reach a month end. Different adjusters can land on the same date — the last day of a month may also be its last Friday — so read each one for what it computes, not for where it happened to land.
Tip. Expect short, complete programs that you trace exactly: what is printed, or whether an exception is thrown at run time. Items chain several calls on one value, so track which results are assigned, where a month end clamps, where a LocalTime wraps, and how Duration and Period print. Calculation items compare Period.between, ChronoUnit.between, until and the getX versus toTotalX or toXPart accessors. Type items pair an amount or unit with a class that may not support it; remember the code still compiles. Zone items fix an explicit ZoneId and a stated DST transition date, then ask for the resulting wall-clock time, offset or elapsed duration, so you must know how gaps and overlaps resolve and whether each addition is date-based or time-based. Multiple-answer items line up near-twins side by side: plusHours versus plusDays, Duration.ofDays versus ChronoUnit.DAYS, withZoneSameInstant versus withZoneSameLocal, equals versus isEqual, next versus nextOrSame, and fixed offsets versus region zones.
- Every java.time object is immutable: plus, minus and with return a new object, and an unassigned result is lost.
- of() factories validate at run time: months are 1-based, hours 0–23, and 29 February needs a leap year — otherwise DateTimeException.
- plusMonths, withMonth and withYear clamp to the month's last valid day; withDayOfMonth throws for a day the month lacks.
- LocalTime arithmetic wraps silently past midnight, but LocalTime differences assume the same day and can be negative.
- Duration prints PTnHnMnS with no day part (ofDays(3) is PT72H); Period prints PnYnMnD, turns weeks into days, and is only normalized (months into years) when you call normalized().
- Period.ofX methods are static, so chaining them keeps only the last call; use Period.of(y, m, d) or the withX/plusX instance methods.
- between() and until() count complete units only; toTotalMonths() folds years into months while getMonths() returns just the component.
- Unsupported units compile but throw UnsupportedTemporalTypeException at run time: no time units on LocalDate, no date units on LocalTime, nothing beyond DAYS on Instant.
- withZoneSameInstant keeps the moment and changes the clock; withZoneSameLocal keeps the clock and changes the moment; equals compares zone and local value, isEqual compares the instant.
- In a DST gap the time moves forward by the gap; in an overlap the earlier offset wins; plusDays and Period keep the wall clock while plusHours and any Duration add elapsed time.
Frequently asked questions
What is the difference between Duration and Period in Java?
Duration is an exact amount of time stored as seconds and nanoseconds, printed like PT1H35M, and a Duration of one day is always 24 hours. Period is a calendar amount stored as years, months and days, printed like P2Y4M9D, and a Period of one day is one calendar day, which can be 23 or 25 hours in a zone that changes to or from daylight saving time. Use Duration with time-based types and Instant, and Period with dates.
Why does LocalDate.plusMonths(1) on the 31st not give the 31st?
Adding months to a LocalDate clamps the day-of-month to the last valid day of the resulting month rather than throwing or rolling into the next month. 2026-08-31 plus one month is 2026-09-30 because September has 30 days, and a clamp into February gives the 28th, or the 29th in a leap year. Each step clamps separately, so adding one month twice can end on a different day from adding two months at once. By contrast, withDayOfMonth() validates and throws DateTimeException for a day the month does not have.
Why does adding a Duration to a LocalDate compile but throw an exception?
LocalDate.plus accepts any TemporalAmount, so the compiler allows a Duration. At run time the Duration adds itself in seconds, a unit a LocalDate cannot hold, so the call throws UnsupportedTemporalTypeException (Unsupported unit: Seconds), even for a whole number of days such as Duration.ofHours(48). The same happens when a LocalTime gets a non-zero Period, when an Instant gets months or years, and when Duration.between is called with two LocalDate values.
What does ZonedDateTime do with a time that falls in a daylight saving gap?
It does not throw. A local time that does not exist because the clocks jumped forward is moved later by the length of the gap, usually one hour, and given the summer offset. For example 02:15 on 8 March 2026 in America/Chicago becomes 03:15-05:00. ZonedDateTime.ofStrict is the factory that throws a DateTimeException for such an invalid combination.
Which offset does Java pick for a time that happens twice when clocks go back?
By default ZonedDateTime keeps the earlier offset, which is the summer offset and the first occurrence of that wall-clock time. 01:20 on 1 November 2026 in America/Chicago resolves to -05:00. Calling withLaterOffsetAtOverlap() selects the second occurrence at -06:00, one hour later on the timeline, and withEarlierOffsetAtOverlap() switches back.
What is the difference between withZoneSameInstant and withZoneSameLocal?
withZoneSameInstant keeps the same moment on the timeline and recalculates the wall-clock time for the new zone, so 15:00 in Los Angeles in August becomes 17:00 in Chicago. withZoneSameLocal keeps the wall-clock time and attaches the new zone, so it becomes 15:00 in Chicago, a different instant two hours earlier. Use the first to answer 'what time is it there' and the second only to reinterpret a local time.
Why is ZonedDateTime.equals false for the same moment in two zones?
ZonedDateTime.equals compares the local date-time, the offset and the zone ID, so the same instant displayed as 09:00 in America/Los_Angeles and 11:00 in America/Chicago is not equal. To ask whether two values represent the same moment, use isEqual, or compare their toInstant() results; isBefore and isAfter likewise compare instants.
Does ChronoUnit.MONTHS.between round up partial months?
No. ChronoUnit.between, until and Period.between count only complete units and discard the remainder, so 31 March to 30 April 2026 is zero months but 30 days. When the end is earlier than the start the result is negative. Period.between returns separate components such as P1Y6M19D, where getMonths() is 6 and toTotalMonths() is 18.
Source
This lesson covers the "Handling Date, Time, Text, Numeric and Boolean Values" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.
- Java SE 25 Developer Professional (1Z0-831) exam page and topics — Oracle University
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.