Java Period
Represent date-based amounts (years, months, days) in Java with Period.
Period is the calendar-shaped sibling of Duration. Where Duration is "X seconds plus Y nanos," Period is "X years, Y months, Z days." It's the right type for any length you'd quote in calendar terms: "trial period of 30 days," "annual subscription," "two-month notice period," "add one billing cycle to the renewal date."
The two never mix. Duration.ofDays(30) is 30 × 24 × 3600 seconds exactly. Period.ofDays(30) is 30 calendar days, which usually but not always equals 30 × 24 hours (DST transitions add or remove an hour). For "exactly that many seconds," Duration. For "the calendar day that's N days later," Period.
Creating
Period.ofDays(30);
Period.ofWeeks(2); // stored as 14 days
Period.ofMonths(3);
Period.ofYears(1);
Period.of(1, 6, 0); // 1 year, 6 months, 0 days
Period.between(startDate, endDate); // takes LocalDate (not LocalDateTime)
Period.parse("P1Y2M3D"); // ISO-8601: P[years]Y[months]M[days]DThe string form is PnYnMnD — P1Y2M3D is one year, two months, three days. The P prefix is mandatory. No T (that would make it a Duration); no hours, minutes, or seconds (those don't fit).
Period.between(start, end) takes two LocalDates and returns a breakdown of the difference:
Period age = Period.between(LocalDate.of(1990, 3, 15), LocalDate.of(2025, 11, 4));
// P35Y7M20D — 35 years, 7 months, 20 daysThat's the standard "compute an age" idiom. The result is a breakdown, not a single number — there are 35 years worth in there, then 7 months on top, then 20 days. To collapse to a single count, use ChronoUnit.YEARS.between(...), which returns a long.
Inspection
Period p = Period.of(1, 6, 14);
p.getYears(); // 1
p.getMonths(); // 6
p.getDays(); // 14
p.toTotalMonths(); // 1 * 12 + 6 = 18 (years + months, ignoring days)
p.isZero(); // false
p.isNegative(); // true if any component is negativeThree accessors for the three components, plus toTotalMonths for a quick aggregation. There's no toTotalDays — that would require knowing the calendar context (a year is 365 or 366 days; a month is 28-31).
Arithmetic
p.plus(Period.ofMonths(1));
p.plusYears(1);
p.plusMonths(6);
p.plusDays(14);
p.minus(Period.ofDays(7));
p.multipliedBy(3);
p.negated();
p.normalized(); // collapse extra months into yearsnormalized() is interesting: it collapses any month count of 12 or more into years. Period.of(0, 14, 0).normalized() is Period.of(1, 2, 0). It does not touch days — there's no "normalize 31 days into 1 month and 1 day" because months aren't constant-length.
Adding to a date
Period is a TemporalAmount. Any date-like Temporal accepts it:
LocalDate maturity = LocalDate.of(2025, 11, 4).plus(Period.ofMonths(6));
LocalDate retirement = LocalDate.of(1990, 3, 15).plus(Period.ofYears(65));
LocalDateTime renewal = LocalDateTime.of(2025, 11, 4, 9, 0).plus(Period.ofYears(1));
ZonedDateTime nextBill = zdt.plus(Period.ofMonths(1));Adding the month or year part of a Period to an Instant throws UnsupportedTemporalTypeException — an Instant is a point on the timeline with no calendar, so the JDK refuses to compute "an instant one month later" without a zone. (The day part is fine: Instant.plus(Period.ofDays(1)) succeeds, because the JDK treats a day as exactly 86,400 seconds. It's only months and years that have no fixed length and so have no meaning on an Instant.) When you need calendar arithmetic, convert via ZonedDateTime:
Instant nextMonth = inst.atZone(ZoneId.of("UTC"))
.plus(Period.ofMonths(1))
.toInstant();That's deliberate verbosity — the conversion is the place where you supply the missing calendar info.
The plusMonths clamping rule from LocalDate applies to Period-arithmetic the same way: January 31 + Period.ofMonths(1) is February 28, not March 3.
Period doesn't normalise across components
A subtle behavior: Period.of(1, 0, 365) is not equal to Period.of(2, 0, 0), even though they describe the same length when added to a typical date. The class stores the breakdown verbatim and compares by structure:
Period.of(1, 0, 365).equals(Period.of(2, 0, 0)); // false
Period.of(0, 14, 0).equals(Period.of(1, 2, 0)); // false (until normalized())
Period.of(0, 14, 0).normalized().equals(Period.of(1, 2, 0)); // trueFor "is this period at least a year regardless of how it's broken down," compare on dates: start.plus(p1).isEqual(start.plus(p2)) is the only fully-correct check.
Distance: Period.between vs ChronoUnit.between
Period diff = Period.between(start, end); // calendar breakdown
long days = ChronoUnit.DAYS.between(start, end); // single long
long months = ChronoUnit.MONTHS.between(start, end);
long years = ChronoUnit.YEARS.between(start, end);The two answer different questions:
Period.between(start, end)returns "1 year, 6 months, 14 days" — useful when you want to display a breakdown.ChronoUnit.DAYS.between(start, end)returns567(or whatever the literal day count is) — useful when you want to compare or accumulate.
Use the second when you need to do arithmetic on the result. Use the first when you want to show a human.
A worked example: subscriptions, trials, and ages
The program below uses Period for a small subscription scenario: trial ends one month from signup, the renewal date repeats every year, the customer's age is computed from a birthdate, and the clamping behavior on month boundaries is made explicit. It also shows the contrast with Duration for "the same span in elapsed time."
What to take from the run:
- Adding
Period.ofMonths(1)to January 31 produced February 28 — the same clamping rule from LocalDate.Period.plusMonths(1).minusMonths(1)is not always the identity. When you're computing billing dates near month-end, design around the clamp explicitly (e.g., always bill on the 1st of the next month) rather than assuming round-trip symmetry. Period.between(birth, today)returned a calendar breakdown — years, months, days. For "are they an adult?" you wantChronoUnit.YEARS.between(birth, today) >= 18, notage.getYears() >= 18. Both give the same answer for this case, but they answer different questions in general —ChronoUnit.YEARS.betweenis the total whole years,age.getYears()is the years component of the breakdown.Period.of(0, 14, 0).normalized()becamePeriod.of(1, 2, 0). The day count was untouched — days can't normalize without knowing which months are in play. If you build aPeriodfrom arithmetic and want a "clean" representation, callnormalizedbefore storing or displaying.P1Y.equals(P12M)wasfalse, andP1Y.equals(P365D)was alsofalse. Equality is structural, not by length. Applied to2024-01-31(a leap year),+ P1Yand+ P12Mboth gave2025-01-31, but+ P365Dgave2025-01-30— a day short, because 2024 has 366 days. So even "the same length" depends on the date you apply it to. When you really want "do these two periods produce the same end date?", compute both end dates and compare withLocalDate.isEqual. The.normalized()form fixes the year/month case but never the day case.- The
inst.plus(Period.ofMonths(1))call threwUnsupportedTemporalTypeException. AnInstanthas no calendar, so a month (whose length varies) has no meaning on it. The day part of aPeriodis fine on anInstant— a day is a fixed 86,400 seconds — but months and years are not. Convert throughZonedDateTimefirst when you need calendar arithmetic; the type system makes you choose a zone explicitly. The mirror failure from the Duration chapter (Durationon aLocalDate) is the same design: the JDK refuses to invent the missing context.
What's next
Period closes out the "lengths of time" pair. The next two chapters cover the string ↔ value boundary: Java Date Formatting for turning java.time values into strings, and Java Date Parsing for the inverse. Both run on DateTimeFormatter, which is the modern, thread-safe replacement for the legacy SimpleDateFormat.