Java Legacy Date-Klasse
Die veraltete java.util.Date-Klasse – warum sie durch java.time ersetzt wurde und wie man zwischen ihnen konvertiert.
java.util.Date war Javas ursprüngliche Datum-Zeit-Klasse, vorhanden seit Java 1.0 im Jahr 1995. Sie ist noch im JDK enthalten; neuer Code sollte sie nicht verwenden, aber du wirst ihr in Bibliotheken, Datenbanken (java.sql.Date erweitert sie) und in jedem Code begegnen, der älter als etwa 2014 ist. Dieses Kapitel soll die Brücke zu java.time schlagen.
Kurz gesagt: Date ist intern ein Wrapper um ein Epoch-Millisekunden-long, ähnlich wie Instant intern ein (seconds, nanos)-Paar ist. Die naheliegende Konvertierung ist daher Date ↔ Instant. Für alles andere (Jahr, Monat, Tag, Kalenderarithmetik) konvertiere zuerst zu java.time.
Was Date wirklich ist
public class Date implements Cloneable, Comparable<Date>, Serializable {
private long fastTime; // milliseconds since 1970-01-01T00:00:00Z
}Das ist der gesamte tatsächliche Zustand. Ein Date ist ein Zeitpunkt, gemessen in Millisekunden seit dem Unix-Epoch, in UTC. Trotz des Namens trägt es kein Kalenderdatum – getYear() und ähnliche Methoden berechnen aus dem Millisekunden-Wert in der Standard-Zeitzone der JVM, was die Quelle der bekannten Probleme dieser API ist.
Date now = new Date(); // current moment
Date epoch = new Date(0); // 1970-01-01T00:00:00Z
Date fromMs = new Date(1_700_000_000_000L);
long ms = now.getTime(); // milliseconds since epochnew Date() und Date.getTime() sind die zwei Methoden, die gut gealtert sind. Alles andere wurde entweder als veraltet markiert oder birgt Fallstricke.
Die veralteten Kalender-Accessoren
Diese Methoden wurden in Java 1.1 (1997) als veraltet markiert, als Calendar eingeführt wurde:
date.getYear(); // year - 1900 (deprecated)
date.getMonth(); // 0-11 (deprecated)
date.getDate(); // 1-31, day of month (deprecated)
date.getDay(); // 0-6, day of week (deprecated)
date.getHours(); date.getMinutes(); date.getSeconds(); // local-zone reads (deprecated)Die Veraltungsmarkierungen existieren seit 28 Jahren. Sie funktionieren noch. Die Fallstricke:
getYear()gibtyear - 1900zurück. Für 2025 gibt es125zurück. Das ist ein klarer Kandidat für „seltsamste API-Entscheidung im JDK."getMonth()gibt 0-11 zurück. Januar ist 0, Dezember ist 11. Off-by-one-Fehler sind garantiert, wenn mangetMonth() + 1schreibt und es einmal vergisst.- Jeder Accessor liest in der Standard-Zeitzone der JVM. Dasselbe
Dateauf zwei Maschinen in verschiedenen Zeitzonen liefert unterschiedlichegetDate()-Ergebnisse.
Ruf diese Methoden nicht auf. Sobald du dich dabei ertappst, date.getYear() aufzurufen, konvertiere zu Instant/ZonedDateTime und verwende die modernen Accessoren.
Die Date ↔ Instant-Brücke
Date legacy = new Date();
Instant inst = legacy.toInstant(); // since Java 8
Instant other = Instant.parse("2025-11-04T19:30:00Z");
Date back = Date.from(other); // since Java 8toInstant() und Date.from(...) sind die modernen Konvertierungsmethoden, die mit java.time hinzugefügt wurden. Das sind die einzigen zwei java.util.Date-Aufrufe, die du in neuem Code schreiben solltest.
Die Konvertierung ist in einer Richtung verlustbehaftet: Date hat Millisekunden-Präzision, Instant hat Nanosekunden-Präzision. Ein Roundtrip Instant → Date → Instant schneidet die sub-millisekunden Nanos ab:
Instant high = Instant.parse("2025-11-04T19:30:00.123456789Z");
Instant low = Date.from(high).toInstant();
// low = 2025-11-04T19:30:00.123Z — the 456789 nanos are goneFür Server-Zeitstempel ist das in Ordnung; bei hochauflösender Ereigniserfassung bleibe durchgehend bei Instant.
java.sql.Date und java.sql.Timestamp
Die JDBC-Typen sind Unterklassen von java.util.Date:
java.sql.Date– ein Datum ohne Uhrzeit (die Zeitkomponente wird in manchen Zeitzonen auf 00:00:00 gesetzt). Irreführender Name; es ist immer noch ein Millisekunden-seit-Epoch-Wrapper darunter.java.sql.Time– eine Uhrzeit ohne Datum.java.sql.Timestamp– wieDate, aber mit Nanosekunden-Präzision.
Alle haben JDK-8-Konvertierungsmethoden:
java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.of(2025, 11, 4));
LocalDate localDate = sqlDate.toLocalDate();
java.sql.Timestamp ts = java.sql.Timestamp.from(Instant.now());
Instant inst = ts.toInstant();Moderne JDBC-Treiber akzeptieren java.time-Typen auch direkt über setObject/getObject – für neuen Code überspringe die java.sql.*-Typen und verwende LocalDate/Instant. Die Konvertierungen sind für Code vorgesehen, der mit einem Treiber oder Framework zusammenarbeiten muss, das noch nicht migriert wurde.
Vergleichen und Sortieren
Date implementiert Comparable<Date>. Die Reihenfolge ist nach Millisekunden seit Epoch – dieselbe wie bei Instant. Das Sortieren von List<Date> funktioniert also genauso wie das Sortieren von List<Instant>.
equals vergleicht den zugrunde liegenden long. Zwei Date-Werte sind gleich, wenn und nur wenn sie denselben Millisekunden-Wert haben. Hashing funktioniert (es ist (int)(time ^ (time >>> 32))), sodass Date als HashMap-Schlüssel geeignet ist – obwohl Instant auch hier die moderne Wahl ist.
Mutabilität
Der größte versteckte Fallstrick: Date ist mutable.
Date d = new Date();
d.setTime(0); // mutates d in placeDas bedeutet, dass jede Methode, die ein Date akzeptiert oder zurückgibt, gefährlich ist – der Aufrufer kann den Wert nach der Übergabe ändern; der Aufgerufene kann den Wert ändern, den der Aufrufer hält. Bibliothekscode kopiert defensiv (new Date(d.getTime())) jedes erhaltene Date. Das ist die Art von Buchführung, die java.time durch konsequente Unveränderlichkeit aller Typen vollständig eliminiert hat.
Behandle in Legacy-Code jedes Date-Feld so, als könnte es sich jederzeit ändern. Konvertiere zu Instant an der API-Grenze, wenn du einen stabilen Snapshot benötigst.
SimpleDateFormat: die andere veraltete Sache
Zusammen mit Date im alten Code findet sich java.text.SimpleDateFormat:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String s = sdf.format(new Date());
Date d = sdf.parse("2025-11-04");SimpleDateFormat ist nicht thread-sicher. Das Teilen einer Instanz über Threads hinweg führt zu falscher Ausgabe, Ausnahmen oder beidem, und das unregelmäßig. Die Standard-Umgehungslösung in Legacy-Code ist ThreadLocal<SimpleDateFormat>; der moderne Ersatz ist DateTimeFormatter, der thread-sicher und cachebar ist.
Wenn du Code mit einem static SimpleDateFormat pflegst, behandle es als bekannten Fehler – unabhängig davon, ob jemand bereits einen Fehler gemeldet hat.
Ein praktisches Beispiel: Legacy-Interoperabilität
Das folgende Programm verwendet Date so, wie man es in einer Legacy-API findet, und zeigt den Konvertierungspfad zu java.time für jede Operation. Lies es als „hier ist das Migrations-Rezept": Jeder Legacy-Aufruf hat ein einzeiliges Instant- oder ZonedDateTime-Äquivalent.
Was aus dem Lauf zu mitnehmen ist:
Date.toString()wird in der Standard-Zeitzone der JVM ausgegeben. DerselbeDate-Wert wird auf einem UTC-Server anders angezeigt als auf einem America/New_York-Laptop. Das ist der zentrale Designfehler, der zum Redesign vonjava.timegeführt hat – der Wert ist UTC, die Anzeige ist lokal, und die API bietet keine einfache Möglichkeit zu erkennen, welche Sicht man gerade betrachtet. Wenn es auf die Zeitzone ankommt, konvertiere zuZonedDateTimeund gib die Zone explizit an.now.getYear()gab125für 2025 zurück. Dieyear - 1900-Konvention war ein Java-1.0-Fehler, der nie behoben wurde; die Methode wurde in 1.1 als veraltet markiert und ist immer noch vorhanden. JedergetYear()-Aufruf auf einemDate, der „125" oder „85" zurückgibt, ist ein Fehler, der darauf wartet, einem Benutzer angezeigt zu werden.Instant.parse("...nanoseconds")gab neun Stellen Präzision aus; derselbe Wert verlor beim Roundtrip durchDate.fromund zurück die letzten sechs. Für Server-Logs (Millisekunden-Präzision ist ausreichend) spielt die Kürzung keine Rolle. Für „Ich habe dieses Ereignis mit hochpräzisem Timing erfasst" – mache keinen Roundtrip durchDate.- Legacy-+1-Tag war
now.getTime() + 24L * 60 * 60 * 1000– manuelle Arithmetik, leicht zu vertippen (dasLvergessen und überlaufen). Modernesinst.plus(Duration.ofDays(1))ist typsicher und liest sich wie gesprochen. Beim Migrieren ist das Ersetzen jedertime + N * ms-Berechnung durchDurationdas einfachste, was man tun kann. - Die Mutabilitäts-Demo am Ende zeigte, dass
shared.setTime(0)den Wert veränderte, der durch beide Referenzen sichtbar war. In einem multi-threaded Codebase ist das eine Race Condition; in single-threaded Code ist es immer noch ein defensives Kopier-Ritual, das das JDK jeder Bibliothek aufgezwungen hat. Die moderne API erfordert dieses Ritual nie.
Was kommt als Nächstes
java.util.Date ist eine Hälfte der Legacy-API. Die andere Hälfte ist java.util.Calendar – die Klasse, die in Java 1.1 hinzugefügt wurde, um Date die Kalender-Accessoren zu geben, die Date selbst nicht hätte haben sollen. Das nächste Kapitel, Java Calendar Class, ist das letzte in diesem Teil und behandelt es in derselben Migrations-Rezept-Form: Jeder Legacy-Aufruf hat einen java.time-Ersatz.