ZonedDateTime ist eine unveränderliche (immutable) Klasse aus dem java.time-Paket, die Datum und Uhrzeit zusammen mit Zeitzoneninformationen (ZoneId) speichert. Anders als LocalDateTime identifiziert ZonedDateTime eindeutig einen Moment auf der Zeitachse. Laut der Spezifikation von Oracle Java 17 (2024) behandelt die Klasse Sommerzeitumstellungen (DST) korrekt durch die Zonenregeln der IANA Time Zone Database.
Wichtige Punkte
ZonedDateTime ist eine der wichtigsten Klassen im java.time-Paket, die Datum und Uhrzeit mit vollständigen Zeitzoneninformationen darstellt. Sie kombiniert drei Komponenten: LocalDateTime (Datum und Uhrzeit), ZoneId (Zonenkennung) und ZoneOffset (Offset relativ zur UTC).
Anders als LocalDateTime, das nur die Wanduhrzeit (wall-clock time) ohne Zeitzonenbindung speichert, identifiziert ZonedDateTime eindeutig einen Moment. Zwei identische LocalDateTime-Instanzen in verschiedenen Zeitzonen repräsentieren unterschiedliche Zeitpunkte. Zwei identische ZonedDateTime-Instanzen repräsentieren denselben Moment.
Die Klasse ist vollständig unveränderlich und thread-safe. Alle arithmetischen Operationen geben ein neues Objekt zurück. ZonedDateTime implementiert das ChronoZonedDateTime-Interface und kann überall dort verwendet werden, wo in Java mit Zonenzeiten gearbeitet wird.
Laut den Oracle Java 17 Spezifikationen unterstützt ZonedDateTime die Arbeit mit jeder Zone aus der IANA Time Zone Database, die über 600 Zeitzonen umfasst.
Der Hauptunterschied — ZonedDateTime enthält eine Zeitzone, während LocalDateTime keine enthält. Dieser grundlegende Unterschied bestimmt den Anwendungsbereich jeder Klasse.
LocalDateTime wird für lokale Ereignisse verwendet: Konzertzeit, Unterrichtsplan, Geburtsdatum. Wenn ein Ereignis in Moskau um 15:00 Uhr stattfindet, zeichnet LocalDateTime 15:00 ohne Bindung auf. Wenn Sie den Server nach New York verschieben, bleibt die Zeit 15:00 — aber es wird ein anderer physikalischer Moment sein.
ZonedDateTime wird für globale Daten verwendet: Server-Logs, API-Zeitstempel, internationale Meetings. Wenn ein Meeting um 15:00 MSK geplant ist, bewahrt ZonedDateTime sowohl die Zeit als auch die Zone. In New York wird es korrekt als 8:00 EST angezeigt. Laut Baeldung (2024) ist die Wahl zwischen LocalDateTime und ZonedDateTime die häufigste architektonische Entscheidung bei der Arbeit mit Daten.
Faustregel: Wenn Daten für eine einzelne Region gespeichert werden — verwenden Sie LocalDateTime. Wenn Daten Zeitzonengrenzen überschreiten — verwenden Sie ZonedDateTime. Wenn Sie einen absoluten Moment darstellen müssen — verwenden Sie Instant.
Die Zeitzone in java.time wird durch die Klasse ZoneId repräsentiert. ZoneId ist eine Zonenkennung im Format „Kontinent/Region“, zum Beispiel „Europe/Moscow“, „America/New_York“, „Asia/Tokyo“. ZoneId wird über die statische Methode of(String zoneId) oder über die Standardzeitzone des Systems ermittelt.
ZoneId wird in zwei Typen unterteilt: fixed offset (fester Offset, z.B. „+03:00“) und region-based (regionale Zonen, z.B. „Europe/London“). Regionale Zonen enthalten Sommerzeitumstellungsregeln und historische Änderungen. Fixed offset ist lediglich ein fester Offset.
Um den aktuellen Offset einer ZoneId zu einem bestimmten Zeitpunkt zu erhalten, verwenden Sie die Methode getRules(), die ZoneRules zurückgibt. ZoneRules enthält alle Übergänge und Offsets für eine bestimmte Zone. Dies ist der Schlüsselmechanismus für die korrekte DST-Behandlung.
Alle Zeitzonen werden mit dem JDK über tzdata-Dateien (IANA Time Zone Database) ausgeliefert und regelmäßig aktualisiert. Unter Android hängt die tzdata-Version von Systemaktualisierungen über Google Play Services ab.
ZonedDateTime kann auf verschiedene Weisen erstellt werden. Die einfachste ist now(), die die aktuelle Zeit in der Systemzeitzone zurückgibt. Die Variante now(ZoneId) ermöglicht es, die aktuelle Zeit in einer bestimmten Zone zu erhalten.
Die Methode of(LocalDateTime, ZoneId) erstellt eine ZonedDateTime aus lokaler Zeit und Zone. Die Variante of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) erstellt aus Komponenten.
LocalDateTime kann über die Methode atZone(ZoneId) in ZonedDateTime konvertiert werden. Instant — über Instant.atZone(ZoneId). Date — über Date.toInstant().atZone(ZoneId).
val moscowZone = ZoneId.of("Europe/Moscow")
val nowInMoscow = ZonedDateTime.now(moscowZone)
val fromComponents = ZonedDateTime.of(
2026, 7, 21, 15, 30, 0, 0, moscowZone
)
val fromLocal = LocalDateTime.now().atZone(moscowZone)
val fromInstant = Instant.now().atZone(moscowZone)
Die wichtigste Konvertierungsmethode ist withZoneSameInstant(ZoneId). Sie konvertiert eine ZonedDateTime in eine andere Zeitzone, während derselbe Moment erhalten bleibt. Zum Beispiel 15:00 MSK → 8:00 EST. Die Methode withZoneSameLocal(ZoneId) ändert die Zone, während die lokale Zeit erhalten bleibt — dies ergibt einen anderen Moment.
Um den Offset relativ zur UTC zu erhalten, verwenden Sie die Methode getOffset(), die ZoneOffset zurückgibt. ZoneOffset ist eine Unterklasse von ZoneId, die einen festen Offset im Format „+HH:mm“ oder „-HH:mm“ darstellt.
Die Konvertierung in Instant erfolgt über die Methode toInstant(). Instant ist ein absoluter Zeitpunkt, unabhängig von der Zeitzone. Die umgekehrte Konvertierung ist Instant.atZone(ZoneId).
val moscow = ZonedDateTime.of(
2026, 7, 21, 15, 0, 0, 0,
ZoneId.of("Europe/Moscow")
)
val newYork = moscow.withZoneSameInstant(
ZoneId.of("America/New_York")
)
val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))
Sommerzeitumstellungen verursachen zwei Probleme: Lücken (gaps) und Überlappungen (overlaps). Eine Lücke entsteht im Frühjahr, wenn die Uhren vorgestellt werden — eine bestimmte Zeit existiert nicht. Eine Überlappung entsteht im Herbst, wenn die Uhren zurückgestellt werden — dieselbe Zeit tritt zweimal auf.
ZonedDateTime behandelt diese Situationen durch eine Auflösungsstrategie. Beim Erstellen eines Objekts während einer Lücke verschiebt java.time die Zeit automatisch um den Offset-Betrag. Beim Erstellen während einer Überlappung wird die erste Option (vor der Umstellung) ausgewählt. Dieses Verhalten kann über withZoneSameInstant geändert werden.
Sie können über zone.getRules().isDaylightSavings(instant) prüfen, ob eine Zeit in der Sommerzeit liegt. Die Methode getOffset() zeigt den tatsächlichen Offset für einen bestimmten Moment an, und getRules().getDaylightSavings(instant) zeigt den DST-Anpassungsbetrag in Millisekunden.
fun checkDST(zdt: ZonedDateTime) {
val rules = zdt.getZone().getRules()
val instant = zdt.toInstant()
if (rules.isDaylightSavings(instant)) {
val dstAmount = rules.getDaylightSavings(instant)
Log.d("Sommerzeit", "Sommerzeit-Offset: $dstAmount")
}
}
Zur Formatierung von ZonedDateTime verwenden Sie DateTimeFormatter. Das Standard-ISO-Format enthält Datum, Uhrzeit und Offset: „2026-07-21T15:30:00+03:00[Europe/Moscow]“. Vordefinierte Formate: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.
Für lokalisierte Ausgabe verwenden Sie DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle kann SHORT, MEDIUM, LONG, FULL sein. LONG enthält den Zonennamen (“MSK“), FULL enthält den vollständigen Namen („Moscow Standard Time“).
Wichtig: Beim Parsen eines Strings mit ZonedDateTime muss das Format Zonen- oder Offset-Informationen enthalten. Wenn die Zone nicht angegeben ist, verwenden Sie LocalDateTime.parse() und dann atZone().
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))
val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
val custom = DateTimeFormatter
.ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)
val parsed = ZonedDateTime.parse(
"2026-07-21T15:30:00+03:00",
DateTimeFormatter.ISO_OFFSET_DATE_TIME
)
Das erste Beispiel — Anzeige der Besprechungszeit für den Benutzer in seiner lokalen Zeitzone. Der Server sendet ZonedDateTime in UTC, der Client konvertiert in die lokale Zeitzone des Geräts.
fun displayMeetingTime(
serverUtc: ZonedDateTime
): String {
val deviceZone = ZoneId.systemDefault()
val localTime = serverUtc.withZoneSameInstant(deviceZone)
val formatter = DateTimeFormatter
.ofPattern("dd.MM.yyyy HH:mm z")
return localTime.format(formatter)
}
Das zweite Beispiel — Berechnung der Zeit bis zum nächsten Ereignis unter Berücksichtigung der Zeitzone. Wir verwenden ZonedDateTime für die Serverzeit und Duration.between() zur Berechnung der Differenz.
fun timeUntilEvent(eventTime: ZonedDateTime): String {
val now = ZonedDateTime.now()
val duration = Duration.between(now, eventTime)
val hours = duration.toHours()
val minutes = duration.toMinutes() % 60
return "Remaining $hours h $minutes min"
}
Das dritte Beispiel — Arbeit mit der Retrofit-API. Der Server gibt einen ISO-8601-String mit Zone zurück. Wir verwenden einen benutzerdefinierten Deserialisierer zur Konvertierung in ZonedDateTime.
data class EventResponse(
@JsonAdapter(ZonedDateTimeAdapter::class)
val eventTime: ZonedDateTime
)
class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
override fun fromJson(reader: JsonReader): ZonedDateTime? {
return ZonedDateTime.parse(
reader.nextString()
)
}
}
Der erste Fehler — Verwendung von ZoneId.systemDefault() im Servercode. Die Serverzeitzone kann von der des Clients abweichen, und die Verwendung der Systemzone auf dem Server führt zu falschen Berechnungen. Geben Sie die Zone immer explizit an oder verwenden Sie UTC als Referenz.
Der zweite Fehler — Ignorieren der Sommerzeit bei der Dauerberechnung. Duration.between() behandelt Übergänge korrekt, aber wenn Sie Zeitstempel manuell subtrahieren, kann die Sommerzeit einen Fehler von 1 Stunde verursachen. Verwenden Sie ChronoUnit.HOURS.between() anstelle manueller Berechnungen.
Der dritte Fehler — Verwechslung von withZoneSameInstant und withZoneSameLocal. Ersteres ändert die Zone unter Beibehaltung des Moments — die Zeit verschiebt sich. Letzteres ändert die Zone unter Beibehaltung der lokalen Zeit — der Moment ändert sich. Die Wahl der falschen Methode ist laut SonarSource (2024) einer der häufigsten Fehler.
Der vierte Fehler — Annahme, dass die Gerätezeitzone immer der Benutzerzeitzone entspricht. Der Benutzer könnte reisen und erwarten, dass die App die Zeit in seiner „Heimat“-Zeitzone anzeigt, nicht in der aktuellen. Bieten Sie in diesem Fall die Zonenauswahl über die Benutzeroberfläche an.
Häufig gestellte Fragen
ZonedDateTime enthält eine regionale Zonenkennung (z.B. „Europe/Moscow“) und behandelt DST. OffsetDateTime speichert nur einen festen Offset (+03:00) ohne regionale Regeln. Für die Datenbankspeicherung wird OffsetDateTime empfohlen.
Verwenden Sie ZonedDateTime.now(ZoneOffset.UTC) oder Instant.now().atZone(ZoneOffset.UTC). Beide Optionen geben den aktuellen Moment mit Null-Offset zurück. Für einen einfachen Zeitstempel verwenden Sie Instant.now() ohne Zonenbindung.
Ja, aber ein benutzerdefinierter Adapter ist erforderlich. Gson unterstützt ZonedDateTime standardmäßig nicht. Moshi unterstützt es über den Rfc3339DateJsonAdapter. Empfohlen wird die Verwendung von Kotlinx Serialization oder der JavaTimeModule-Bibliothek für Jackson.
java.time verschiebt die Zeit automatisch um den Offset-Betrag nach vorne. Wenn z.B. 02:30 nicht existiert, weil die Uhren auf 03:00 vorgestellt werden, erstellt ZonedDateTime ein Objekt um 03:30. Sie können über ZoneRules.getTransition(instant) auf eine Lücke prüfen.
JDBC 4.2 unterstützt OffsetDateTime, aber nicht direkt ZonedDateTime. ZonedDateTime enthält eine regionale Zone, die kein SQL-Äquivalent hat. Es wird empfohlen, OffsetDateTime oder Instant zu speichern und die Zone in einer separaten Spalte zu speichern.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch