Java Period Class
Java Period Class
Period represents a date-based amount of time — so many years, so many months, so many days — the kind of quantity used to describe "2 years and 6 months" rather than a precise span like "63,072,000 seconds." It is the class for anything expressed in calendar terms: a warranty period, a notice period, a subscription term measured in months. Introduced in Java 8 alongside the rest of java.time, Period is immutable and, unlike Duration, deliberately has no fixed length in seconds — a month means something different depending on which month it gets applied to.
What Is Period?
java.time.Period stores three separate fields — years, months, and days — without collapsing them into a single count, because a month has no fixed length and Period is built to represent calendar-relative amounts, not fixed durations. Period.between() computes one by measuring the gap between two LocalDate values, but Period.of(), ofYears(), ofMonths(), ofWeeks(), and ofDays() construct one directly, with no dates involved at all. A constructed Period can then be added to or subtracted from a LocalDate to compute a new date — this is the direction most real Period usage actually goes: you already know a warranty lasts one year, and you want the expiry date, not a measured gap between two dates you already have.
One sentence before the diagram: Period models a calendar-relative amount, which only becomes a concrete number of days once it is actually applied to a real date.
Period.of(1, 6, 0) = "1 year, 6 months"
|
v
purchaseDate (2026-01-15) .plus(period)
|
v
expiryDate = 2027-07-15
The SAME Period applied to a different start date can span
a different number of actual days - "6 months" is 181 days
starting from January, but 184 days starting from March
Why Period Was Introduced
Before Java 8, applying a calendar-based amount like "1 year and 6 months" to a date meant chaining several Calendar.add() calls, with no single value anywhere representing that amount on its own.
1// File: BeforePeriod.java
2import java.util.*;
3
4public class BeforePeriod {
5 public static void main(String[] args) {
6 Calendar purchaseDate = Calendar.getInstance();
7 purchaseDate.clear();
8 purchaseDate.set(2026, Calendar.JANUARY, 15);
9
10 // "1 year, 6 months" exists nowhere as a single value - it has to be
11 // applied field by field, every single time it's needed
12 purchaseDate.add(Calendar.YEAR, 1);
13 purchaseDate.add(Calendar.MONTH, 6);
14
15 System.out.println("Warranty expires: " + purchaseDate.get(Calendar.YEAR) + "-"
16 + (purchaseDate.get(Calendar.MONTH) + 1) + "-" + purchaseDate.get(Calendar.DAY_OF_MONTH));
17 }
18}Output:
Warranty expires: 2027-7-15
Period turns "1 year, 6 months" into a real, storable, reusable value that can be applied to any date with a single call.
1// File: AfterPeriod.java
2import java.time.*;
3
4public class AfterPeriod {
5 public static void main(String[] args) {
6 LocalDate purchaseDate = LocalDate.of(2026, 1, 15);
7 Period warrantyLength = Period.of(1, 6, 0);
8
9 LocalDate expiryDate = purchaseDate.plus(warrantyLength);
10
11 System.out.println("Warranty expires: " + expiryDate);
12 }
13}Output:
Warranty expires: 2027-07-15
Both versions land on the same expiry date. Only the second one gives "1 year, 6 months" an actual name — warrantyLength — that could be stored, passed around, or applied to a hundred other purchase dates without repeating the arithmetic.
Syntax
Period can be constructed directly, applied to a date with plus() or minus(), and normalized when its raw field values exceed the usual range.
1// File: PeriodSyntaxForms.java
2import java.time.*;
3
4public class PeriodSyntaxForms {
5 public static void main(String[] args) {
6 Period fullPeriod = Period.of(1, 6, 15);
7 Period justMonths = Period.ofMonths(3);
8 Period fromWeeks = Period.ofWeeks(2);
9
10 System.out.println("of(1,6,15): " + fullPeriod);
11 System.out.println("ofMonths(3): " + justMonths);
12 System.out.println("ofWeeks(2): " + fromWeeks);
13
14 LocalDate startDate = LocalDate.of(2026, 1, 15);
15 System.out.println("startDate.plus(fullPeriod): " + startDate.plus(fullPeriod));
16 System.out.println("startDate.minus(justMonths): " + startDate.minus(justMonths));
17
18 Period unnormalized = Period.of(0, 14, 0);
19 System.out.println("14 months, before normalizing: " + unnormalized);
20 System.out.println("14 months, normalized: " + unnormalized.normalized());
21
22 System.out.println("isZero: " + Period.ZERO.isZero());
23 System.out.println("isNegative: " + Period.of(0, 0, -5).isNegative());
24 System.out.println("toTotalMonths on 1y6m: " + Period.of(1, 6, 0).toTotalMonths());
25 }
26}Output:
of(1,6,15): P1Y6M15D
ofMonths(3): P3M
ofWeeks(2): P14D
startDate.plus(fullPeriod): 2027-07-30
startDate.minus(justMonths): 2025-10-15
14 months, before normalizing: P14M
14 months, normalized: P1Y2M
isZero: true
isNegative: true
toTotalMonths on 1y6m: 18
Period.of(0, 14, 0) stores exactly 0 years and 14 months — it does not automatically roll the excess into a year on construction. normalized() is the method that actually performs that conversion, and it is worth calling deliberately before displaying a Period built this way to a user.
Common Use Cases
Computing a Reminder Date Ahead of an Expiry
Subtracting a Period from a known end date is the standard way to compute a lead-time reminder, and it correctly clamps to the last valid day of the resulting month when needed.
1// File: ReminderBeforeExpiryExample.java
2import java.time.*;
3
4public class ReminderBeforeExpiryExample {
5 public static void main(String[] args) {
6 LocalDate contractExpiry = LocalDate.of(2027, 3, 31);
7 Period reminderLeadTime = Period.ofMonths(1);
8
9 LocalDate reminderDate = contractExpiry.minus(reminderLeadTime);
10
11 System.out.println("Contract expires: " + contractExpiry);
12 System.out.println("Send renewal reminder on: " + reminderDate);
13 }
14}Output:
Contract expires: 2027-03-31
Send renewal reminder on: 2027-02-28
Normalizing a Period Before Displaying It
Calling normalized() turns an awkward raw month count into the year-and-month form a user would actually expect to read.
1// File: NormalizingBeforeDisplayExample.java
2import java.time.*;
3
4public class NormalizingBeforeDisplayExample {
5 public static void main(String[] args) {
6 Period rawServiceLength = Period.ofMonths(25);
7
8 System.out.println("Raw: " + rawServiceLength);
9 System.out.println("Normalized: " + rawServiceLength.normalized());
10 }
11}Output:
Raw: P25M
Normalized: P2Y1M
Validating a Computed Period Before Trusting It
isNegative() and isZero() catch invalid date ranges immediately, before a negative or empty period causes confusing downstream behavior.
1// File: PeriodValidationExample.java
2import java.time.*;
3
4public class PeriodValidationExample {
5 public static void main(String[] args) {
6 LocalDate contractStart = LocalDate.of(2026, 8, 25);
7 LocalDate contractEnd = LocalDate.of(2026, 6, 1);
8
9 Period gap = Period.between(contractStart, contractEnd);
10
11 if (gap.isNegative()) {
12 System.out.println("Invalid contract: end date is before start date");
13 } else if (gap.isZero()) {
14 System.out.println("Contract starts and ends on the same day");
15 } else {
16 System.out.println("Contract length: " + gap);
17 }
18 }
19}Output:
Invalid contract: end date is before start date
Comparing Two Periods by Total Months
toTotalMonths() reduces years and months into one comparable number, useful whenever two structurally different Period values might still represent the same overall length.
1// File: ToTotalMonthsComparisonExample.java
2import java.time.*;
3
4public class ToTotalMonthsComparisonExample {
5 public static void main(String[] args) {
6 Period planA = Period.of(1, 2, 0);
7 Period planB = Period.ofMonths(14);
8
9 System.out.println("Plan A in months: " + planA.toTotalMonths());
10 System.out.println("Plan B in months: " + planB.toTotalMonths());
11 System.out.println("Same total months: " + (planA.toTotalMonths() == planB.toTotalMonths()));
12 System.out.println("planA.equals(planB): " + planA.equals(planB));
13 }
14}Output:
Plan A in months: 14
Plan B in months: 14
Same total months: true
planA.equals(planB): false
planA and planB represent the same total length but store their years and months differently, which is exactly why equals() says they differ while toTotalMonths() says they match — equals() compares the raw stored fields, not the overall calendar-relative amount.
Real-World Example
An electronics retailer tracks warranty coverage that varies by product category — a laptop might carry a one-year warranty, an appliance a longer two-and-a-half-year one — and needs to compute each product's expiry date and check whether a submitted claim still falls within coverage.
1// File: WarrantyPolicy.java
2import java.time.*;
3
4public record WarrantyPolicy(String category, Period coveragePeriod) {}1// File: WarrantyService.java
2import java.time.*;
3
4public class WarrantyService {
5
6 public LocalDate calculateExpiryDate(LocalDate purchaseDate, WarrantyPolicy policy) {
7 return purchaseDate.plus(policy.coveragePeriod());
8 }
9
10 public boolean isClaimValid(LocalDate purchaseDate, WarrantyPolicy policy, LocalDate claimDate) {
11 LocalDate expiryDate = calculateExpiryDate(purchaseDate, policy);
12 return !claimDate.isAfter(expiryDate);
13 }
14}1// File: WarrantyDemo.java
2import java.time.*;
3
4public class WarrantyDemo {
5 public static void main(String[] args) {
6 WarrantyPolicy electronicsWarranty = new WarrantyPolicy("Electronics", Period.ofYears(1));
7 WarrantyPolicy applianceWarranty = new WarrantyPolicy("Appliance", Period.of(2, 6, 0));
8
9 WarrantyService warrantyService = new WarrantyService();
10
11 LocalDate purchaseDate = LocalDate.of(2025, 2, 10);
12
13 LocalDate electronicsExpiry = warrantyService.calculateExpiryDate(purchaseDate, electronicsWarranty);
14 LocalDate applianceExpiry = warrantyService.calculateExpiryDate(purchaseDate, applianceWarranty);
15
16 System.out.println("Electronics warranty expires: " + electronicsExpiry);
17 System.out.println("Appliance warranty expires: " + applianceExpiry);
18
19 LocalDate claimDate = LocalDate.of(2026, 3, 1);
20 System.out.println("Electronics claim on " + claimDate + " valid: "
21 + warrantyService.isClaimValid(purchaseDate, electronicsWarranty, claimDate));
22 System.out.println("Appliance claim on " + claimDate + " valid: "
23 + warrantyService.isClaimValid(purchaseDate, applianceWarranty, claimDate));
24 }
25}Output:
Electronics warranty expires: 2026-02-10
Appliance warranty expires: 2027-08-10
Electronics claim on 2026-03-01 valid: false
Appliance claim on 2026-03-01 valid: true
The same claim date falls outside the electronics warranty but well inside the appliance one, exactly reflecting each policy's own coverage length. A mistake that appears often in fresher pull requests is hardcoding a warranty length as a raw number of days — 365 for "one year" — which quietly breaks the moment a leap year is involved. Period.ofYears(1) added directly to the purchase date always lands on the same calendar date one year later, regardless of how many actual days that span happens to contain.
Combining Period With Other Features
Period.between() ties directly back to LocalDate, and the confusion between Period.getDays() and ChronoUnit.DAYS.between() is covered in the dedicated LocalDate article. Period cannot be added to a bare LocalTime, since LocalTime has no year, month, or day fields for a Period to apply to — only LocalDate and LocalDateTime accept plus(Period) and minus(Period). Period does not implement Comparable, unlike LocalDate, LocalTime, and LocalDateTime, since "which is bigger, one month or 31 days" has no single correct answer without a reference date — toTotalMonths() or applying both periods to the same reference date are the two standard workarounds.
Best Practices
Construct a Period directly with of(), ofYears(), or ofMonths() when the intent is to express a reusable business rule — a warranty length, a notice period — rather than always reaching for Period.between(), which is for measuring a gap that already exists between two known dates.
Call normalized() before displaying a Period to a user if it was built with of() or ofMonths() using a value that could exceed 11 months. Raw excess months, like P14M, read far less naturally than the normalized P1Y2M.
Never try to compare two Period values directly, since Comparable is not implemented and would not have a single correct answer anyway. Apply both periods to the same reference date and compare the resulting dates, or compare toTotalMonths() when the days component genuinely does not matter.
Reach for Duration instead of Period the moment a rule needs a fixed, precise span rather than a calendar-relative one. A 30-day free trial that should always last exactly 720 hours needs Duration; a one-month free trial that should land on the same calendar day next month needs Period.
Common Mistakes
Repeatedly adding the same Period to a date, chaining each result off the previous one, lets a clamped day silently drift and stay drifted for the rest of the sequence.
1// File: RepeatedPlusDriftMistake.java
2import java.time.*;
3import java.util.*;
4
5public class RepeatedPlusDriftMistake {
6 public static void main(String[] args) {
7 LocalDate firstBillingDate = LocalDate.of(2026, 1, 31);
8 Period oneMonth = Period.ofMonths(1);
9
10 // WRONG - chaining plus() off the previous result lets a clamped
11 // day (28 in February) drift forward permanently into later months
12 List<LocalDate> driftedDates = new ArrayList<>();
13 LocalDate current = firstBillingDate;
14 for (int i = 0; i < 4; i++) {
15 driftedDates.add(current);
16 current = current.plus(oneMonth);
17 }
18
19 // CORRECT - compute every date fresh from the original day-of-month
20 List<LocalDate> correctDates = new ArrayList<>();
21 for (int i = 0; i < 4; i++) {
22 correctDates.add(firstBillingDate.plusMonths(i));
23 }
24
25 System.out.println("Drifted: " + driftedDates);
26 System.out.println("Correct: " + correctDates);
27 }
28}Output:
Drifted: [2026-01-31, 2026-02-28, 2026-03-28, 2026-04-28]
Correct: [2026-01-31, 2026-02-28, 2026-03-31, 2026-04-30]
Once February clamps January 31st down to the 28th, every later date in the drifted sequence keeps building from that clamped value instead of the original 31st — March and April never get a chance to show their real end-of-month day. Computing each billing date fresh from firstBillingDate.plusMonths(i), rather than chaining off the previous result, is what keeps every date correct on its own.
Trying to compare two Period values directly does not compile, since Period does not implement Comparable.
1// File: PeriodNotComparableMistake.java
2import java.time.*;
3
4public class PeriodNotComparableMistake {
5 public static void main(String[] args) {
6 Period shortLeave = Period.ofDays(10);
7 Period longLeave = Period.ofMonths(1);
8
9 // shortLeave.compareTo(longLeave);
10 // This does not compile - Period does not implement Comparable,
11 // since "which is bigger, 10 days or 1 month" has no fixed answer
12 // without applying both to an actual reference date
13
14 LocalDate referenceDate = LocalDate.of(2026, 2, 1);
15 boolean longLeaveIsLonger = referenceDate.plus(longLeave).isAfter(referenceDate.plus(shortLeave));
16
17 System.out.println("Is 1 month longer than 10 days, starting Feb 1: " + longLeaveIsLonger);
18 }
19}Output:
Is 1 month longer than 10 days, starting Feb 1: true
Trying to add a Period to a LocalTime is another compile-time dead end worth knowing about ahead of time, since LocalTime has no year, month, or day fields for a date-based amount to apply to at all — only LocalDate and LocalDateTime declare plus(Period) and minus(Period).
Interview Questions
Q1. What is Period, and how is it different from Duration?
Period represents a date-based amount — years, months, and days — meant to be applied to calendar dates, while Duration represents a precise, time-based amount measured in seconds and nanoseconds. Adding a Period of one month to a date always lands on the same day next month regardless of how many actual days that spans, while adding a Duration of 30 days always advances by exactly 720 hours. Interviewers use this question to check whether a candidate picks the right one based on whether the rule is calendar-relative or precisely fixed.
Q2. Does Period automatically normalize its years, months, and days when constructed with of()?
No. Period.of(0, 14, 0) stores exactly 0 years and 14 months, with no automatic conversion into 1 year and 2 months. normalized() is a separate method that must be called explicitly to perform that conversion — construction alone never does it.
Q3. Why doesn't Period implement Comparable?
Because there is no single, universally correct answer to "which is longer, one month or 31 days" without applying both to an actual calendar date — a month can be anywhere from 28 to 31 days depending on which one it is. Duration can implement Comparable because seconds have a fixed, unambiguous length; Period cannot, because months and years do not.
Q4. What happens when you repeatedly add the same Period to a date across several iterations, starting from a date near the end of a month?
Once a month-end date gets clamped down because a target month is shorter — January 31st becoming February 28th, for instance — chaining further plus() calls off that already-clamped result keeps building from the smaller day for every subsequent iteration, rather than returning to the original day-of-month whenever a later month is actually long enough to support it. The fix is computing each date fresh from the original starting date using plusMonths(i) for each iteration, rather than accumulating off the previous result.
Q5. Can a Period be added to a LocalTime?
No, and this fails to compile rather than failing at runtime. LocalTime has no year, month, or day fields for a Period's date-based amount to apply to — only LocalDate and LocalDateTime declare a plus(Period) method.
Q6. What is the difference between Period.getDays() and Period.toTotalMonths()?
getDays() returns only the raw days field stored inside the Period, completely separate from whatever years and months it also holds. toTotalMonths() combines the years and months fields into one total month count, while deliberately ignoring the days field entirely. Neither one answers "how many days total does this period span" — that question requires applying the Period to an actual reference date and measuring the result.
FAQs
Is Period immutable?
Yes. Like every class in java.time, Period is immutable — every method that looks like it modifies a Period, such as plusDays() on the Period itself, actually returns a new instance and leaves the original untouched.
Can a Period have negative values?
Yes. Period.of(0, 0, -5) is valid and represents negative five days, and Period.between() returns a period with negative components whenever the end date comes before the start date. isNegative() checks whether any of the three fields is negative.
What is Period.ZERO?
A public static constant representing a period of zero years, zero months, and zero days — useful as a default or a sentinel value, and Period.ZERO.isZero() always returns true.
Can I add a Period to a LocalDateTime?
Yes. LocalDateTime.plus(Period) applies the years, months, and days to the date portion of the value while leaving the time portion completely unchanged, since a Period has nothing to say about hours, minutes, or seconds.
Does Period.between() ever return a period that needs normalizing?
No. Period.between() always computes a result with months already constrained to 0 through 11 and days constrained to a valid range for the calculation, so it never needs a separate call to normalized(). That method only matters for periods built directly with of() or ofMonths() using values outside the usual range.
What is the difference between Period.of(0, 14, 0) and Period.of(1, 2, 0)?
Both represent the same total length — 14 months — but they store it differently, with the first keeping all 14 months in the months field and the second splitting it into 1 year and 2 months. toTotalMonths() returns 14 for both, but equals() considers them different, since it compares the raw stored fields rather than the overall calendar-relative amount.
Can two different Period objects represent the same amount of time?
In terms of total months, yes, exactly as Period.of(0, 14, 0) and Period.of(1, 2, 0) both total 14 months despite storing that total differently. In terms of equals(), no — two Period objects are only equal when their years, months, and days fields all match exactly, not merely when they add up to the same overall amount.
Summary
Period gives a calendar-based amount of time its own reusable value — one year, six months, however it needs to be expressed — that can be constructed once and applied to any LocalDate or LocalDateTime afterward, rather than recomputed field by field every time it is needed. of() and its variants build one directly; plus() and minus() apply it to a date; normalized() cleans up a raw, unnormalized value before it reaches a display screen.
The habit worth carrying forward is computing every date in a recurring sequence fresh from the original starting date, rather than chaining plus() calls off the previous result, exactly the fix the billing-date drift example demonstrates. Period has no Comparable implementation and no place in time-of-day arithmetic — Duration, covered next, picks up precisely where those two limitations leave off.
What to Read Next
Learn how to measure a gap between two times.