Viele Java-Anwendungen nutzen noch die veralteten Klassen java.util.Date
, java.sql.Date
und java.util.Calendar
, welche in Java 1.0 und 1.1 eingeführt wurden, um Datum- und Zeitinformationen darzustellen und zu verarbeiten. Diese APIs sind komplex und die fehlende Unterstützung für oft benötigte Operationen, beispielsweise die Berechnung der Zeit zwischen zwei Zeitpunkten, behindern nicht nur die Entwicklung, sondern sind auch häufige Fehlerursachen. In Java 8 wurde mit java.time
eine neue API eingeführt, die diese Probleme lösen soll.
In diesem Blog-Artikel möchte ich auf einige Probleme der alten APIs hinweisen und aufzeigen, was bei der Migration zu java.time
zu beachten ist.
Probleme von java.util.Date
Die Klasse java.util.Date
ist seit den frühen Tagen von Java Teil der Standardbibliothek und dient der repräsentativen Handhabung von Zeitpunkten und Zeitangaben. Sie wurde entworfen, um sowohl Datums- als auch Zeitinformationen in einem einzigen Objekt zu speichern. Trotz ihrer weit verbreiteten Verwendung hat sich jedoch gezeigt, dass java.util.Date
viele Einschränkungen und Probleme mit sich bringt, darunter Unsicherheiten bei der Behandlung von Zeitzonen, Mutabilität und eine unübersichtliche API. Diese Mängel haben in der modernen Softwareentwicklung den Bedarf an einer besser strukturierten und benutzerfreundlicheren Lösung, wie sie durch die java.time
API bereitgestellt wird, verstärkt.
Problem 1 Veränderlichkeit
Instanzen von java.util.Date
sind veränderlich. Während dies aus Sicht der Flexibilität nützlich erscheint, birgt es erhebliche Risiken in Multi-Threading-Umgebungen. Greifen mehrere Threads auf dieselbe Date-Instanz zu, können u.a. schwer diagnostizierbare Race Conditions die Folge sein. Um dies zu vermeiden, müssen Synchronisationsmechanismen eingeführt werden, was zusätzlichen Aufwand und eine erhöhte Komplexität zur Folge hat.
In dem Code-Beispiel kann die Ausgabe variieren.
Instanzen von java.time
-Klassen wie z.B. java.time.Instant
hingegen sind unveränderlich. Änderungen an Objekten dieser Klassen erzeugen stattdessen neue Objekte, womit Thread-Sicherheit garantiert werden kann.
Problem 2 Unübersichtliche API
Die Methoden von java.util.Date
sind nicht ausreichend, um gängige Aufgaben intuitiv zu bearbeiten. Wie im vorherigen Beispiel gezeigt, erweist sich die Addition von 10 Sekunden zu einem bestehenden Zeitpunkt als umständlich, da man dafür 10.000 Millisekunden addieren muss. Zudem sind Methoden wie date.getSeconds()
und date.setSeconds()
bereits als deprecated
markiert.
Häufig ist es erforderlich, komplexe Logik (wie etwa How can I calculate a time span in Java and format the output? ) zu entwickeln, um grundlegende Anforderungen wie die Berechnung der Zeitspanne zwischen zwei Daten zu erfüllen. Dies führt zu unübersichtlichem Code und erschwert die Wartbarkeit der Anwendung.
java.time
hingegen beinhaltet Klassen wie Period
und Duration
für diesen Anwendungsfall. So können Zeitspannen zwischen 2 Zeitstempeln ohne viel Aufwand berechnet, umgerechnet und ausgegeben werden.
Problem 3 Zeitdarstellung und Zeitzonen
java.util.Date
speichert einen Unix-Zeitstempel ohne Zeitzoneninformation. Wenn die Methode toString()
aufgerufen wird, wird das Datum in der lokalen Zeitzone des Systems ausgegeben. Dies kann dazu führen, dass unterschiedliche Systeme unterschiedliche Darstellungen der selben Instanz zurückliefern. Im Gegensatz dazu bieten die Klassen java.time.ZonedDateTime
und java.time.OffSetDateTime
eine explizite Handhabung von Zeitzonen. Hierbei werden Zeit und Zeitzone in einem einzigen Objekt gespeichert, wodurch sie unabhängig von der Umgebung bleiben. Zusätzlich wird die Umwandlung zwischen Zeitzonen vereinfacht.
Problem 4 java.sql.Date
Die Klasse java.sql.Date
stellt eine Schnittstelle zu Datenbanken über JDBC dar und entspricht dem SQL DATE, das nur das Datum ohne Zeitinformationen speichert. Im Gegensatz dazu enthält java.util.Date
auch Zeitinformationen, was zu potenziellen Informationsverlusten bei der naheliegenden Konvertierung zwischen diesen beiden Objekten führen kann. Um eine korrekte Konvertierung sicherzustellen, sollte stattdessen von java.util.Date
zu java.sql.Timestamp
gewechselt werden, da letzterer sowohl Datum als auch Zeit erfasst und somit Verlust von Informationen vermeidet. Diese fehleranfällige Typ-Konvertierung entfällt mit java.time
, da java.time
-Objekte ab JDBC Version 4.2 auch direkt genutzt werden können, um Datenbankzugriffe zu realisieren.
Migration
Der erste Schritt der Migration besteht darin, alle Vorkommen von java.util.Date
im Code zu identifizieren. Hierbei sollte auch auf Verwendungen von java.sql
-Klassen wie java.sql.Date
, java.sql.Timestamp
etc. geachtet werden, da auch diese Klassen konvertiert werden können. Bei diesem Schritt kann eine automatisierte Architekturprüfung mit ArchUnit unterstützen ( Automatisierte Architekturprüfungen mit ArchUnit – Praxiswissen ). Für die Migration zur neuen API wurde den alten Klassen eine Methode toInstant()
zur Verfügung gestellt, die eine java.time.Instant Instanz aus Objekten der alten Klassen erzeugt. Aus diesem Objekt kann dann, je nach Anwendungsfall, ein Objekt der geeigneten Klasse der java.time
API generiert werden:
Datumstypen in Java & SQL |
Vor Java 8 |
Modern |
SQL |
Zeitpunkt in UTC |
java.util.Date java.sql.Timestamp |
java.time.Instant |
TIMESTAMP WITH TIME ZONE |
Zeitpunkt mit Abweichung von UTC |
- | java.time.OffsetDateTime | TIMESTAMP WITH TIME ZONE |
Zeitpunkt mit Zeitzone |
java.util.GregorianCalendar |
java.time.ZonedDateTime |
TIMESTAMP WITH TIME ZONE |
Datum & Uhrzeit |
- | java.time.LocalDateTime | TIMESTAMP WITHOUT TIME ZONE |
Datum |
java.sql.Date |
java.time.LocalDate | DATE |
Uhrzeit |
java.sql.Time | java.time.LocalTime | TIME WITHOUT TIME ZONE |
Uhrzeit mit Abweichung |
- | java.time.OffsetTime | TIME WITH TIME ZONE |
Für eine schrittweise Migration kann mithilfe der Methode Date.from()
aus einer Instanz von java.time.Instant
ein java.util.Date
-Objekt erzeugt werden. Dies erlaubt es Entwicklern, die neuen Funktionen und Vorteile der java.time
API teilweise zu integrieren, während sie gleichzeitig die bestehende Codebasis unterstützt. Auch wenn so Interoperabilität ermöglicht wird, muss darauf geachtet werden bei der Konvertierung zwischen den Klassen von java.util.Date
/ java.sql.Date
und java.time
die richtigen Typen zu wählen, um Informationsverlust zu vermeiden.
Eine umfangreiche Testabdeckung ist essentiell, um sicherzustellen, dass alle Berechnungen und Darstellungen von Datumswerten den Erwartungen entsprechen. Dies gilt insbesondere für Anwendungen, welche unterschiedliche Zeitzonen unterstützen.
Fazit
Die Migration zur java.time
API erfordert sorgfältige Überlegungen, insbesondere in Bezug auf Typen, Schnittstellen und Datenbankoperationen. Die Vorteile der neuen API überwiegen jedoch. Die klaren und sicheren Datums- und Zeitoperationen von java.time
steigern die Entwicklungsgeschwindigkeit und verbessern die Wartbarkeit und Zuverlässigkeit. Somit ist die Migration eine wichtige Investition in die Zukunft ihrer Java-Anwendungen.
Um weitere Vorteile moderner Java-Versionen vorzustellen bieten wir eine Schulung zum Update auf Java 17 an:
zurück zur Blogübersicht