W3docs

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 epoch

new 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() gibt year - 1900 zurück. Für 2025 gibt es 125 zurü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 man getMonth() + 1 schreibt und es einmal vergisst.
  • Jeder Accessor liest in der Standard-Zeitzone der JVM. Dasselbe Date auf zwei Maschinen in verschiedenen Zeitzonen liefert unterschiedliche getDate()-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 8

toInstant() 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 gone

Fü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 – wie Date, 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 place

Das 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.

java— editable, runs on the server

Was aus dem Lauf zu mitnehmen ist:

  • Date.toString() wird in der Standard-Zeitzone der JVM ausgegeben. Derselbe Date-Wert wird auf einem UTC-Server anders angezeigt als auf einem America/New_York-Laptop. Das ist der zentrale Designfehler, der zum Redesign von java.time gefü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 zu ZonedDateTime und gib die Zone explizit an.
  • now.getYear() gab 125 für 2025 zurück. Die year - 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. Jeder getYear()-Aufruf auf einem Date, 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 durch Date.from und 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 durch Date.
  • Legacy-+1-Tag war now.getTime() + 24L * 60 * 60 * 1000 – manuelle Arithmetik, leicht zu vertippen (das L vergessen und überlaufen). Modernes inst.plus(Duration.ofDays(1)) ist typsicher und liest sich wie gesprochen. Beim Migrieren ist das Ersetzen jeder time + N * ms-Berechnung durch Duration das 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.

Übung

Übung
Eine alte Bibliothek gibt `java.util.Date` von `getCreatedAt()` zurück. Du möchtest wissen: 'Liegt das im selben Kalendertag wie heute in New York?' Was ist der richtige Weg?
Eine alte Bibliothek gibt `java.util.Date` von `getCreatedAt()` zurück. Du möchtest wissen: 'Liegt das im selben Kalendertag wie heute in New York?' Was ist der richtige Weg?
Was this page helpful?