W3docs

Einführung in die Java Date and Time API

Einführung in die moderne Java-Datums- und Zeit-API in java.time als Ersatz für die veralteten Klassen Date und Calendar.

Java 8 hat java.time eingeführt, ein neues Paket zur Darstellung von Daten, Uhrzeiten, Dauern, Zeitzonen und der Arithmetik zwischen ihnen. Es ersetzte zwei frühere APIs — java.util.Date und java.util.Calendar —, die zu Recht als der am schlechtesten entworfene Teil des JDK galten. Die neue API wurde von Stephen Colebornes früherer Joda-Time-Bibliothek geprägt; wer Joda bereits kennt, wird java.time vertraut vorkommen.

Die zwei wichtigsten Aspekte des Redesigns:

  1. Jeder Typ ist unveränderlich. Ein einmal erstelltes LocalDate ändert sich nie. Methoden wie plusDays(7) geben ein neues LocalDate zurück. Das macht die API konstruktionsbedingt thread-sicher und beseitigt eine ganze Klasse von Fehlern.
  2. Jeder Typ bedeutet eine Sache. LocalDate ist ein Datum ohne Uhrzeit. Instant ist ein Moment auf dem Zeitstrahl. Duration ist eine Zeitspanne. Das veraltete Date war irgendwie alles davon gleichzeitig, je nachdem welcher Konstruktor verwendet wurde; die neue API trennt sie, damit der Typ sagt, welche Art von Wert man hat.

Dieses Kapitel ist die Übersicht. Die nächsten zehn Kapitel gehen auf jede Klasse im Detail ein.

Die Kerntypen

"A date"           LocalDate            2025-11-04
"A time of day"    LocalTime            14:30:00
"Both, no zone"    LocalDateTime        2025-11-04T14:30:00
"Both, with zone"  ZonedDateTime        2025-11-04T14:30:00-05:00 [America/New_York]
"A moment"         Instant              2025-11-04T19:30:00Z      (UTC, seconds-since-epoch)
"A length of time" Duration             PT1H30M                   (1 hour 30 minutes)
"A length of date" Period               P1Y2M3D                   (1 year 2 months 3 days)

Die horizontale Trennung — Local* vs. Zoned/Instant — ist die wichtigste. Lokale Typen tragen keine Zeitzone. Ein LocalDate von 2025-11-04 ist "der vierte November"; er sagt nicht, ob das der vierte in Tokio oder in Honolulu ist. Er ist der richtige Typ für einen Geburtstag, ein Vertragsdatum oder einen UI-Datepicker.

Zeitzonenbehaftete Typen tragen ihre Zone. ZonedDateTime ist "dieser Kalendermoment an diesem Ort" — genau das, was man für "Meeting angesetzt für 9 Uhr in New York" braucht. Instant ist ein Moment auf dem globalen Zeitstrahl — UTC-Sekunden seit der Epoche — was man für Logging, Nachrichtenzeitstempel oder alles braucht, was global geordnet sein muss, ohne lokale Beschriftungen zu benötigen.

Die horizontale Trennung zwischen Duration und Period ist ebenfalls wichtig. Duration ist eine Zeitspanne, die man in Sekunden vergleichen kann — PT24H sind genau 24 × 3600 Sekunden. Period ist eine Länge in Kalenderbegriffen — P1M (ein Monat) ist in manchen Monaten 30 Tage und in anderen 31. Für Zeitmessungen braucht man Duration. Für "einen Monat zu einem Abrechnungsdatum addieren" braucht man Period.

Die fluente Form

Jeder Typ wird über eine konsistente fluente API erzeugt und verändert:

LocalDate today    = LocalDate.now();
LocalDate stardate = LocalDate.of(2025, 11, 4);
LocalDate parsed   = LocalDate.parse("2025-11-04");

LocalDate nextWeek = today.plusDays(7);                     // immutable: returns a NEW LocalDate
LocalDate lastYear = today.minusYears(1);
LocalDate firstOfMonth = today.withDayOfMonth(1);            // with* returns a copy with one field changed

boolean before = today.isBefore(stardate);
int year = today.getYear();

Drei Muster, die man überall sieht:

  • now() — aktueller Wert aus der Systemuhr.
  • of(...) — explizite Komponenten.
  • parse(...) — aus einem String (standardmäßig ISO-8601).

Und für Transformationen:

  • plusX(n) / minusX(n) — Arithmetik.
  • withX(value) — ein einzelnes Feld ersetzen.
  • isBefore(other) / isAfter(other) — Vergleich.

Dieses Muster wiederholt sich in LocalDate, LocalTime, LocalDateTime, ZonedDateTime und Instant. Wer das Muster kennt, liest jede Klasse wie denselben Dialekt mit einem leicht anderen Vokabular.

Zeitzonen sind schwierig, und die API gibt das zu

Der wichtigste Grund, warum java.util.Date so problematisch war, ist, dass es Zeitzonen unsichtbar zu machen versuchte. Das Ergebnis war die berühmte Fehlerklasse: "ein Date speichern, es auf einem Server in einer anderen Zeitzone abrufen, das falsche Kalenderdatum erhalten." java.time löst das, indem die Zone im Typ explizit gemacht wird.

Wenn man ein Datum von einem Benutzer entgegennimmt und nicht weiß, in welcher Zone er sich befindet, speichert man es als LocalDate. Wenn der Benutzer sagt, es ist "9 Uhr seiner Zeit" und man kennt seine Zone, speichert man es als ZonedDateTime mit der Zone. Wenn man ein Server-Ereignis protokolliert, speichert man es als Instant. Man sollte kein LocalDateTime speichern und darauf hoffen, dass die Zeitzone durchkommt; die fehlende Zone ist der eigentliche Fehler.

Instant now = Instant.now();                                 // unambiguous: a moment in UTC
ZonedDateTime localized = now.atZone(ZoneId.of("Europe/Berlin"));  // a label for that moment in Berlin

Die Zonenhierarchie:

  • ZoneOffset ist ein fester ±HH:MM-Offset von UTC: +05:30, -08:00. Keine Sommerzeitbehandlung.
  • ZoneId ist eine benannte Zone: Europe/Berlin, America/New_York. Enthält den IANA-Datenbankdatensatz darüber, welchen Offset diese Zone an einem bestimmten Tag hat, einschließlich Sommerzeit-Übergängen und historischen Änderungen.

Immer ZoneId gegenüber ZoneOffset bevorzugen, wenn man die Wahl hat. "America/New_York" ist über die Sommerzeit hinweg korrekt; "−05:00" ist nur außerhalb der Sommerzeit korrekt.

Die veralteten Typen sind nicht verschwunden

java.util.Date, java.util.Calendar und java.text.SimpleDateFormat existieren noch immer. Neuer Code sollte sie nicht verwenden — aber viel alter Code tut es, und man muss mit ihm interoperieren. Die Konvertierungsmethoden sind direkt:

// java.util.Date <-> java.time.Instant
Instant inst = legacyDate.toInstant();
Date    back = Date.from(inst);

// java.util.Calendar -> java.time.ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.ofInstant(
    cal.toInstant(), cal.getTimeZone().toZoneId());

Das Muster ist einseitig: legacy → java.time ist unkompliziert; für alles Neue bleibt man in java.time und konvertiert nur an der API-Grenze, wo der alte Code lebt. Die Kapitel Legacy Date und Calendar am Ende dieses Teils behandeln die Brücke im Detail.

Ein durchgearbeitetes Beispiel: die Typfamilie in einem Programm

Das folgende Programm verwendet jeden Typ, den die obige Übersicht eingeführt hat — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period — und zeigt, wie sie ineinander konvertiert werden. Es ist die "Tour"-Version; jeder einzelne Typ bekommt sein eigenes Kapitel von hier an.

java— editable, runs on the server

Was man aus der Ausgabe mitnehmen sollte:

  • Der erste Block hat die Familie durch Komposition aufgebaut: LocalDate + LocalTime = LocalDateTime; LocalDateTime + ZoneId = ZonedDateTime; ZonedDateTimeInstant. Das ist das Gitter der Konvertierungen, und man führt diese jedes Mal durch, wenn man eine API-Grenze überschreitet. Die Pfeile zeigen für die meisten Paare in beide Richtungen — Instant.atZone(zone) und ZonedDateTime.toLocalDateTime() schließen die Schleifen.
  • Ein Instant hat drei verschiedene "aussehende" Zeiten ausgegeben, wenn er aus New York, Berlin und Tokio betrachtet wird. Das ist der Sinn von Instant: er ist der Moment, unabhängig davon, wo man steht. Das ZonedDateTime fügt die "wo ich stehe"-Beschriftung hinzu. Die beiden zu verwechseln ist der Fehler des veralteten Date.
  • Duration hat als PT1H30M und Period als P3M ausgegeben. Das ISO-8601-Dauerformat ist PnYnMnDTnHnMnS — alles vor dem T sind Kalendereinheiten (Period), alles danach sind Zeiteinheiten (Duration). Der String ist genau das, was toString() zurückgibt, und genau das, was parse(...) akzeptiert.
  • today.plusDays(7) hat ein anderes LocalDate erzeugt. Wenn man danach today erneut ausgibt, sieht man, dass das Original unverändert ist — das ist die Unveränderlichkeitsgarantie. Jedes plus/minus/with gibt ein neues Objekt zurück; der Empfänger wird nie verändert. Keine defensiven Kopien, nie Bedenken zur Thread-Sicherheit.
  • ChronoUnit.DAYS.between(today, launch) war die "Abstand"-Operation. Sie gibt ein long zurück, kein Period, weil die Antwort in Tagen keine Kalenderambiguität hat (im Gegensatz zu Monaten, die unterschiedlich lang sind). Jedes Kapitel in diesem Teil verwendet ChronoUnit irgendwo — es ist der Katalog der Zeiteinheiten, über die die API spricht.

Wie es weitergeht

Das nächste Kapitel, Java LocalDate, beginnt die Vertiefungstour. LocalDate ist der einfachste der fünf "Moment"-Typen und der richtige Ausgangspunkt, um die fluente Form zu lernen, die alle anderen teilen.

Übungen

Übung
Sie müssen den Moment speichern, in dem ein Server eine HTTP-Anfrage empfangen hat, damit das Log global über Server in verschiedenen Zeitzonen sortiert werden kann. Welcher `java.time`-Typ passt?
Sie müssen den Moment speichern, in dem ein Server eine HTTP-Anfrage empfangen hat, damit das Log global über Server in verschiedenen Zeitzonen sortiert werden kann. Welcher `java.time`-Typ passt?
Was this page helpful?