Java Period
Kalenderbasierte Zeitspannen (Jahre, Monate, Tage) in Java mit Period darstellen.
Period ist das kalenderorientierte Gegenstück zu Duration. Während Duration „X Sekunden plus Y Nanosekunden" bedeutet, steht Period für „X Jahre, Y Monate, Z Tage." Es ist der richtige Typ für jede Länge, die man in Kalenderbegriffen ausdrückt: „Probezeitraum von 30 Tagen", „Jahresabonnement", „zweimonatige Kündigungsfrist", „einen Abrechnungszyklus zum Verlängerungsdatum hinzufügen."
Die beiden lassen sich nie mischen. Duration.ofDays(30) sind exakt 30 × 24 × 3600 Sekunden. Period.ofDays(30) sind 30 Kalendertage, was normalerweise – aber nicht immer – 30 × 24 Stunden entspricht (Sommerzeit-Übergänge fügen eine Stunde hinzu oder entfernen eine). Für „genau so viele Sekunden" nimmt man Duration. Für „den Kalendertag, der N Tage später liegt", nimmt man Period.
Erstellen
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]DDie Zeichenkettenform lautet PnYnMnD — P1Y2M3D steht für ein Jahr, zwei Monate, drei Tage. Das Präfix P ist obligatorisch. Kein T (das würde es zu einer Duration machen); keine Stunden, Minuten oder Sekunden (die passen hier nicht).
Period.between(start, end) nimmt zwei LocalDates und gibt eine Aufschlüsselung der Differenz zurück:
Period age = Period.between(LocalDate.of(1990, 3, 15), LocalDate.of(2025, 11, 4));
// P35Y7M20D — 35 years, 7 months, 20 daysDas ist das Standardmuster zur Altersberechnung. Das Ergebnis ist eine Aufschlüsselung, keine einzelne Zahl – darin stecken 35 Jahre, dann noch 7 Monate und dann noch 20 Tage. Um auf eine einzelne Zahl zu reduzieren, verwendet man ChronoUnit.YEARS.between(...), das einen long zurückgibt.
Inspektion
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 negativeDrei Zugriffsmethoden für die drei Komponenten sowie toTotalMonths für eine schnelle Aggregation. Ein toTotalDays gibt es nicht – das würde den Kalenderkontext erfordern (ein Jahr hat 365 oder 366 Tage; ein Monat hat 28–31).
Arithmetik
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() ist interessant: Es fasst jeden Monatsanteil von 12 oder mehr in Jahre zusammen. Period.of(0, 14, 0).normalized() ergibt Period.of(1, 2, 0). Es berührt nicht die Tage – es gibt kein „normalisiere 31 Tage in 1 Monat und 1 Tag", weil Monate keine konstante Länge haben.
Zu einem Datum addieren
Period ist ein TemporalAmount. Jedes datumähnliche Temporal akzeptiert es:
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));Das Addieren des Monats- oder Jahresanteils eines Period zu einem Instant wirft eine UnsupportedTemporalTypeException – ein Instant ist ein Punkt auf der Zeitachse ohne Kalender, weshalb das JDK es ablehnt, „einen Instant einen Monat später" ohne Zeitzone zu berechnen. (Der Tagesanteil ist in Ordnung: Instant.plus(Period.ofDays(1)) gelingt, weil das JDK einen Tag als genau 86.400 Sekunden behandelt. Nur Monate und Jahre haben keine feste Länge und sind daher auf einem Instant bedeutungslos.) Wenn Kalenderarithmetik benötigt wird, wandelt man über ZonedDateTime um:
Instant nextMonth = inst.atZone(ZoneId.of("UTC"))
.plus(Period.ofMonths(1))
.toInstant();Das ist bewusste Ausführlichkeit – die Umwandlung ist der Ort, an dem man die fehlenden Kalenderinformationen angibt.
Die plusMonths-Clamp-Regel aus LocalDate gilt bei Period-Arithmetik genauso: 31. Januar + Period.ofMonths(1) ist der 28. Februar, nicht der 3. März.
Period normalisiert nicht komponentenübergreifend
Ein subtiles Verhalten: Period.of(1, 0, 365) ist nicht gleich Period.of(2, 0, 0), obwohl beide beim Addieren zu einem typischen Datum dieselbe Länge beschreiben. Die Klasse speichert die Aufschlüsselung wörtlich und vergleicht nach Struktur:
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)); // trueFür „ist dieser Zeitraum mindestens ein Jahr, unabhängig von seiner Aufschlüsselung", vergleicht man über Daten: start.plus(p1).isEqual(start.plus(p2)) ist die einzig vollständig korrekte Prüfung.
Abstand: 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);Die beiden beantworten unterschiedliche Fragen:
Period.between(start, end)liefert „1 Jahr, 6 Monate, 14 Tage" – nützlich, wenn man eine Aufschlüsselung anzeigen möchte.ChronoUnit.DAYS.between(start, end)liefert567(oder die tatsächliche Tageszahl) – nützlich, wenn man vergleichen oder akkumulieren möchte.
Man nutzt das Zweite, wenn man mit dem Ergebnis rechnen muss. Das Erste verwendet man, wenn man einem Menschen etwas anzeigen möchte.
Ein durchgearbeitetes Beispiel: Abonnements, Testphasen und Alter
Das folgende Programm verwendet Period für ein kleines Abonnementszenario: Die Testphase endet einen Monat nach der Anmeldung, das Verlängerungsdatum wiederholt sich jedes Jahr, das Alter des Kunden wird aus einem Geburtsdatum berechnet, und das Clamp-Verhalten an Monatsgrenzen wird explizit gemacht. Es zeigt auch den Kontrast mit Duration für „dieselbe Zeitspanne in abgelaufener Zeit."
Was man aus der Ausgabe mitnehmen sollte:
- Das Addieren von
Period.ofMonths(1)zum 31. Januar ergab den 28. Februar – dieselbe Clamp-Regel wie in LocalDate.Period.plusMonths(1).minusMonths(1)ist nicht immer die Identitätsfunktion. Wenn man Abrechnungsdaten nahe Monatsende berechnet, sollte man explizit um den Clamp herum entwerfen (z. B. immer am 1. des nächsten Monats abrechnen) statt von Round-Trip-Symmetrie auszugehen. Period.between(birth, today)lieferte eine Kalenderaufschlüsselung – Jahre, Monate, Tage. Für „ist die Person volljährig?" verwendet manChronoUnit.YEARS.between(birth, today) >= 18, nichtage.getYears() >= 18. Beide geben für diesen Fall dieselbe Antwort, beantworten aber im Allgemeinen unterschiedliche Fragen –ChronoUnit.YEARS.betweenliefert die Gesamtzahl ganzer Jahre,age.getYears()die Jahreskomponente der Aufschlüsselung.Period.of(0, 14, 0).normalized()wurde zuPeriod.of(1, 2, 0). Die Tageszahl blieb unberührt – Tage können ohne Kenntnis der jeweiligen Monate nicht normalisiert werden. Wenn man einenPeriodaus Arithmetik aufbaut und eine „saubere" Darstellung möchte, ruft man vor dem Speichern oder Anzeigennormalizedauf.P1Y.equals(P12M)warfalse, undP1Y.equals(P365D)war ebenfallsfalse. Gleichheit ist strukturell, nicht nach Länge. Angewendet auf2024-01-31(ein Schaltjahr) ergaben+ P1Yund+ P12Mbeide2025-01-31, aber+ P365Dergab2025-01-30– einen Tag zu wenig, weil 2024 366 Tage hat. Ob zwei Perioden dasselbe Enddatum erzeugen, hängt also von dem Datum ab, auf das man sie anwendet. Wenn man wirklich wissen möchte, ob zwei Perioden dasselbe Enddatum ergeben, berechnet man beide Enddaten und vergleicht sie mitLocalDate.isEqual. Dienormalized()-Form behebt den Jahr/Monat-Fall, aber niemals den Tage-Fall.- Der Aufruf
inst.plus(Period.ofMonths(1))warf eineUnsupportedTemporalTypeException. EinInstanthat keinen Kalender, daher hat ein Monat (dessen Länge variiert) auf ihm keine Bedeutung. Der Tagesanteil einesPeriodist auf einemInstantin Ordnung – ein Tag entspricht genau 86.400 Sekunden – aber Monate und Jahre nicht. Man wandelt zuerst überZonedDateTimeum, wenn man Kalenderarithmetik benötigt; das Typsystem zwingt einen dazu, explizit eine Zeitzone anzugeben. Der spiegelbildliche Fehler aus dem Duration-Kapitel (Durationauf einemLocalDate) folgt demselben Designprinzip: Das JDK weigert sich, den fehlenden Kontext zu erfinden.
Was kommt als Nächstes
Period schließt das „Zeitlängen"-Paar ab. Die nächsten zwei Kapitel behandeln die Zeichenkette ↔ Wert-Grenze: Java Date Formatting für die Umwandlung von java.time-Werten in Zeichenketten und Java Date Parsing für die umgekehrte Richtung. Beide basieren auf DateTimeFormatter, dem modernen, thread-sicheren Ersatz für das veraltete SimpleDateFormat.