Java LocalDateTime
Datum und Uhrzeit ohne Zeitzoneninformation in Java mit LocalDateTime kombinieren.
LocalDateTime ist LocalDate und LocalTime zusammengefügt — ein Kalenderdatum und eine Uhrzeit, aber immer noch ohne Zeitzone. Es ist der natürliche Wert für „dieses Ereignis fand um 14:30 Uhr am 4. November statt", und es ist der am häufigsten verwendete java.time-Typ in realen Codebasen, die keine globalen Nutzer berücksichtigen müssen.
Die fluente API hat dieselbe Form wie die beiden Hälften — dieselben now/of/parse-Factorymethoden, dieselben plusX/minusX/withX-Modifikatoren, dieselben isBefore/isAfter-Vergleiche. Die interessanten Teile sind die neuen: das Kombinieren mit einem Datum oder einer Uhrzeit, das Zerlegen zurück in die Bestandteile und die explizite Konvertierung zu ZonedDateTime, wenn die Zeitzone schließlich eine Rolle spielt.
Erstellen
LocalDateTime now = LocalDateTime.now(); // JVM default zone
LocalDateTime made = LocalDateTime.of(2025, 11, 4, 14, 30);
LocalDateTime made2 = LocalDateTime.of(2025, Month.NOVEMBER, 4, 14, 30, 15);
LocalDateTime made3 = LocalDateTime.of(LocalDate.of(2025, 11, 4), LocalTime.of(14, 30));
LocalDateTime parsed = LocalDateTime.parse("2025-11-04T14:30:00"); // ISO-8601Die ISO-8601-Form hat ein wörtliches T zwischen Datum und Uhrzeit — das ist das standardmäßige Trennzeichen. LocalDateTime.parse("2025-11-04 14:30:00") (Leerzeichen statt T) lässt sich mit dem Standard nicht parsen; dafür wäre ein benutzerdefinierter DateTimeFormatter erforderlich.
Zerlegung
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();Dies ist die Umkehrung von LocalDate.atTime(time) / LocalTime.atDate(date). Beides ist reine Projektion — kein Informationsverlust, keine eingeführte Zeitzone.
Alle Accessoren auf einen Blick
LocalDateTime erbt das Accessor-Menü beider Hälften:
dt.getYear(); dt.getMonth(); dt.getMonthValue();
dt.getDayOfMonth(); dt.getDayOfWeek(); dt.getDayOfYear();
dt.getHour(); dt.getMinute(); dt.getSecond(); dt.getNano();Gleiche Namen, gleiche Semantik. Es ist nicht nötig, toLocalDate() oder toLocalTime() aufzurufen, um an die einzelnen Teile zu gelangen — sie sind alle direkt verfügbar.
Arithmetik über die Grenze hinaus
Der entscheidende Unterschied zu LocalTime: LocalDateTime.plusHours(3) bei 23:00 läuft nicht lautlos über. Es rollt in den nächsten Tag:
LocalDateTime late = LocalDateTime.of(2025, 11, 4, 23, 0);
late.plusHours(3); // 2025-11-05T02:00 — date advanced as expectedDas ist der Hauptgrund für die Verwendung von LocalDateTime statt LocalTime bei jeder Berechnung, die möglicherweise Mitternacht überschreitet. Die Mathematik entspricht dem, was man von einer echten Uhr erwarten würde, die weiß, welcher Tag es ist.
dt.plusDays(7); dt.plusHours(36); dt.plusMinutes(150);
dt.minusYears(1); dt.minusSeconds(45);
dt.withYear(2026); dt.withHour(0); dt.withMinute(0);Die plusMonths-Clamp-Regel aus LocalDate gilt auch hier: LocalDateTime.of(2025, 1, 31, 12, 0).plusMonths(1) ergibt 2025-02-28T12:00, nicht 2025-03-03T12:00. Der Clamp betrifft nur die Datumskomponente; die Uhrzeit bleibt unverändert.
Die Zeitzone fehlt absichtlich
Ein LocalDateTime ist kein Moment auf der globalen Zeitachse. LocalDateTime.of(2025, 11, 4, 9, 0) könnte 9 Uhr morgens in New York, 9 Uhr morgens in Berlin oder 9 Uhr morgens in Tokio sein — drei sehr unterschiedliche Instant-Werte, und LocalDateTime verrät nicht, welcher es ist. Wenn zwei LocalDateTime-Werte gleich sind, bedeutet das, dass die Datums- und Uhrzeitstrings gleich sind; es bedeutet nicht, dass die zugrundeliegenden Momente gleich sind.
Das ist ein Feature, kein Bug. Für „der Vertrag wird um 14:00 Uhr Ortszeit unterzeichnet, wo auch immer der Unterzeichner sich befindet" ist LocalDateTime genau die richtige Form. Für „der Server hat die Anfrage um ... empfangen" ist es die falsche Form — verwende Instant. Für „das Meeting beginnt um 14:00 Uhr New Yorker Zeit" ist es ebenfalls falsch — verwende ZonedDateTime.
Um in einen zonalen Moment zu konvertieren, muss die Zone explizit hinzugefügt werden:
ZonedDateTime ny = ldt.atZone(ZoneId.of("America/New_York"));
Instant inst = ldt.atZone(ZoneId.systemDefault()).toInstant();atZone(...) ist der entscheidende Aufruf — es ist der Moment, an dem das Typsystem dich zwingt, zu entscheiden, welche Zeitzone du meinst. Sobald du dich entschieden hast, ist die Konvertierung zu Instant mechanisch. Die nächsten beiden Kapitel (ZonedDateTime, Instant) behandeln die zonalen und globalen Formen im Detail.
Vergleichen
dt.isBefore(other);
dt.isAfter(other);
dt.isEqual(other);
dt.compareTo(other);Die Reihenfolge ist lexikographisch nach (date, time). Die gleiche Warnung wie zuvor: Zwei LocalDateTime-Werte werden anhand ihrer String-Darstellungen von Datum und Uhrzeit verglichen, nicht anhand der zugrundeliegenden Momente — denn ohne Zeitzone gibt es keine zugrundeliegenden Momente.
Abstände
ChronoUnit.X.between funktioniert direkt:
long minutes = ChronoUnit.MINUTES.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);
Duration d = Duration.between(start, end);Duration.between funktioniert mit LocalDateTime (es funktioniert mit jedem Temporal). Für rein kalendarische Arithmetik — „wie viele Monate liegen zwischen diesen zwei LocalDateTime-Werten" — verwende ChronoUnit.MONTHS.between, das einen long zurückgibt, oder Period.between(start.toLocalDate(), end.toLocalDate()) für die kalendarisch aufgeschlüsselte Form.
Ein durchgearbeitetes Beispiel: Planung über Mitternacht hinaus
Das folgende Programm verwendet LocalDateTime für ein kleines Stück Planungscode: eine Nachtschicht, die um 22:00 Uhr beginnt und um 06:00 Uhr endet, mit korrekter Dauerberechnung über Mitternacht hinaus; das Aufrunden von „jetzt" auf das nächste Viertelstundenintervall; das Finden des nächsten Vorkommens eines wiederkehrenden 09:30-Uhr-Meetings; und die Demonstration der Regel, dass die Zeitzone explizit hinzugefügt werden muss, wenn in einen Moment in der Zeit konvertiert wird.
Was man aus der Ausführung mitnehmen kann:
Duration.between(startShift, endShift)ergabPT8H. Die Schicht überschritt Mitternacht, und die Datumskomponenten trugen den Übertrag weiter — es gab keine Mehrdeutigkeit. Dieselbe Berechnung mit bloßenLocalTime-Werten hättePT-16Hzurückgegeben (die LocalTime-Falle aus dem vorherigen Kapitel). Für Arithmetik, die möglicherweise Mitternacht überschreitet, istLocalDateTimeder richtige Typ.- Ausgehend von
20:00bliebplusHours(3)bei11-04(23:00, kein Mitternachtsübergang);plusHours(5)rollte vorwärts zu11-05T01:00. Dieplus/minus-Familie aufLocalDateTimepropagiert Überträge korrekt durch die gesamteY/M/D/h/m/s/ns-Kette. Im eigenen Code ist kein Sonderfall erforderlich. - Der Block „nächstes 09:30-Meeting" baute das heutige 09:30 mit drei
withX-Aufrufen und wählte dann anhand vonisBeforezwischenheuteundmorgen. Das ist das typische Muster für „das nächste wiederkehrende Ereignis zu dieser Tageszeit" — klein genug zum Inlinen, häufig genug, um in einen Helfer auszulagern, wenn man viele davon hat. - Der Block „gleiche LocalDateTime, verschiedene Zeitzonen" erzeugte zwei verschiedene
Instant-Werte mit sechs Stunden Abstand. Das ist der zentrale Grund, warumLocalDateTimenicht behauptet, ein Moment zu sein. Die Klasse weigert sich vorzutäuschen, dass ein Datum und eine Uhrzeit allein ausreichende Information sind; es ist ein Label auf einer Wanduhr irgendwo, und welche Wand, hängt von der angegebenen Zeitzone ab. - Die abschließende Unveränderlichkeitsprüfung zeigte
nowunverändert nachplusDays(7).withHour(0).withMinute(0). Diese Garantie gilt für jede Operation, jede Kette, jeden Helfer — es gibt keine Möglichkeit, einLocalDateTimezu verändern. Gib es frei weiter, teile es zwischen Threads, speichere es in einerMap.
Was kommt als Nächstes
LocalDateTime ist der letzte der drei „Local"-Typen — keine Zeitzone, kein Anspruch darauf, ein Moment zu sein. Das nächste Kapitel, Java ZonedDateTime, fügt die Zeitzone explizit hinzu: ein LocalDateTime plus eine ZoneId plus den aufgelösten Offset für diese Ortszeit in dieser Zone, die zusammen einen tatsächlichen Moment auf der globalen Zeitachse bestimmen.