Time is money: Java 8 Date and Time API

Für Sie interessant:

Java EE Erweiterungen

Sie erweitern Ihre bereits vorhandenen Kenntnisse in Java durch die Inhalte dieses Seminars.

882254955_1d49618736_z

Bildquelle: Flickr.com / The Marmot nach CC BY 2.0

Java 8 ist mittlerweile schon ein knappes Jahr alt. Neben den direkt herausstechenden Neuerungen, wie z. B. Streams und Lambda-Ausdrücken, gehört auch eine neue Date and Time API zum Umfang der neuesten Java Version. Die Klasse java.util.Date ist seit Version 1.0 Teil des JDKs, in der Version 1.1 wurde sie bereits teilweise durch die Klasse Calendar ersetzt. Allerdings ließ auch die Calendar-Klasse viele Wünsche offen, so sind z. B. ihre Abstraktionen weder threadsicher noch für viele Anwender intuitiv verständlich. Der Dezember wird immer noch als elfter Monat referenziert und der Januar ist der Monat Null. Um die Designschwächen der in die Jahre gekommenen API zu umgehen, haben sich viele Entwickler mit der Nutzung von externen Bibliotheken geholfen. Die vielleicht populärste dieser Bibliotheken ist die Joda Time API, da verwundert es auch nicht, dass deren Entwickler Stephen Colebourne als Specification Lead für die Java 8 Date and Time API fungierte. Für Java 8 wurde die Joda Time API leicht überarbeitet und in das java.time Package integriert. Wer also vorher schon die Joda Time Bibliothek benutzt hat, wird sich schnell heimisch fühlen, alle anderen werden sich aufgrund der zahlreichen Neuerungen über kurz oder lang umstellen müssen. Es wird also höchste Zeit, sich mit der neuen API auseinanderzusetzen.

Im neuesten JDK Release befindet sich nun also das java.time Package. Die Struktur sieht wie folgt aus:

  • java.time: Bietet Kernfunktionalität für Datums- und Zeitangaben
  • java.time.chrono: Klassen für alternative oder eigene Kalendersysteme
  • java.time.format: Klassen zum Formatieren und Parsen
  • java.time.temporal: Definition von Fields und Units; hauptsächlich für Entwickler von externen Bibliotheken und Frameworks interessant
  • java.time.zone: Klassen für die Unterstützung von Zeitzonen

Die Date and Time API benutzt das ISO-8601 Kalendersystem, welches wiederum auf dem gregorianischen Kalender basiert und sich als de facto Standard für Datums- und Zeit-Angaben etabliert hat. Wer sich damit nicht zufrieden gibt, kann alternativ auch etwas exotischere Kalendersysteme, wie z. B. Thai Buddhist aus dem java.time.chrono Package, verwenden.

Werfen wir zuerst einen Blick darauf, was die Ziele der Entwicklung der neuen API waren.

Design Prinzipien

Die Java 8 Date and Time API wurde hinsichtlich folgender Design Prinzipien entwickelt:

  • Die Methoden sollen wohldefiniert, verständlich und erwartungskonform sein.
  • Der „Fluent Programming Style“, der z. B. bereits bei den Streams verwendet wurde, soll den Code lesbarer machen. So können Methodenaufrufe aneinander gereiht werden, da die Methoden keinen null-Wert zurückgeben und diesen nicht als Parameter erlauben. Dadurch sollen sich flüssig lesbare Ketten von Methodenaufrufen ergeben.
  • Die meisten Klassen der Date and Time API erzeugen unveränderliche Objekte. Es muss also immer ein neues Objekt erzeugt werden, sollte sich an den Werten etwas verändern. Dadurch sind diese Objekte per Definition threadsicher.
  • Es wurde viel Wert auf Erweiterbarkeit gelegt, so kann z. B. ein eigenes Kalendersystem definiert werden.

Was ist Zeit?

Es gibt grundsätzlich verschiedene Arten, Zeit zu erfassen:

  • Kontinuierlich: Die Art, auf die Maschinen Zeit erfassen. Ein (willkürlich) gewählter Zeitpunkt bildet den Ursprung einer Zeitlinie, ausgehend von diesem Ursprung wird in Nanosekundengenauigkeit kontinuierlich weitergezählt.
  • Menschlich: Menschen haben sich an eine Schreibweise für Zeitangaben anhand von Feldern gewöhnt. So gibt es Felder für Jahr, Monat, Tag, Stunde, Minute, Sekunde usw.

Für jede dieser beiden Arten beinhaltet das java.time Package entsprechende Klassen, um Zeit auf unterschiedliche Arten auszudrücken. Dabei sollte bestenfalls im Vorhinein klar sein, welche Art von Zeit und daraus resultierend welche Klassen benutzt werden sollen.

Kontinuierliche Zeit

Kontinuierliche Zeit wird im java.time Package durch die Klassen Instant und Duration repräsentiert. Ein Wert der Instant Klasse beschreibt einen Zeitpunkt auf der Zeitlinie seit dem 01.01.1970 um 00:00 Uhr in Nanosekunden. Dieser Ursprung wird auch als „EPOCH“ bezeichnet und wurde willkürlich bestimmt. Ein Zeitpunkt vor diesem Ursprung wird mit einem negativen Wert dargestellt. Mit Duration kann die Zeit zwischen zwei Instants bestimmt werden. Die aktuelle Zeit seit dem Ursprung kann z. B. wie in folgendem Beispiel dargestellt werden:

Instant timestamp = Instant.now();
Instant epoch = Instant.EPOCH;
Duration between = Duration.between(epoch, timestamp);

Die Ausgabe der Werte der Variablen sieht wie folgt aus:

epoch: 1970-01-01T00:00:00Z
now: 2015-03-23T13:09:38.916Z
between: PT396421H9M38.916S

Hier wird offensichtlich der aktuelle Zeitpunkt seit dem Ursprung bestimmt. Die beiden Variablen now und between beschreiben den gleichen Zeitpunkt auf der Zeitachse, allerdings auf unterschiedliche Weise berechnet und ausgegeben. Die Instant Klasse besitzt außerdem verschiedene Methoden zum Manipulieren von Zeitpunkten. Es gibt mehrere „plus“ und „minus“ Methoden, die, basierend auf der Zeiteinheit, Zeit von einem Zeitpunkt abziehen oder hinzufügen.

Instant later = Instant.now().plusSeconds(10).plusMillis(10).plusNanos(10);

In diesem Beispiel werden zu der aktuellen Zeit jeweils zehn Sekunden, Millisekunden und Nanosekunden addiert, für die Subtraktion sind entsprechende „minus“-Methoden ebenfalls vorhanden.

Menschliche Zeit

Waren die zur Verfügung stehenden Klassen für kontinuierliche Zeit noch ziemlich übersichtlich, enthalten die Darstellungen menschlicher Zeit beinahe jede mögliche Kombination von bestimmten Zeit-, Datums- und Zeitzonenangaben. Das wird anhand folgender Tabelle deutlich:

Klasse/Enum Year Month Day Hours Minutes Seconds Zone Offset Zone ID
LocalDate X X X
LocalTime X X X
LocalDateTime X X X X X X
ZonedDateTime X X X X X X X X
MonthDay X X
Year X
YearMonth X X
Month X
OffsetDateTime X X X X X X X
OffsetTime X X X X

Ein „X“ in der Tabelle bedeutet, dass die Klassen den jeweiligen Datentyp verwenden. Aufgrund dieser Fülle an Klassen wird schnell deutlich, warum im Vorfeld bereits klar sein sollte, welche Art der Zeit- und Datumsangabe gebraucht wird. Soll z. B. ein Geburtstag am 28.06. dargestellt werden, der jedes Jahr wiederkehrend und unabhängig der Zeitangabe ist, könnte dazu die Klasse MonthDay verwendet werden. Ist auch das Alter von Interesse und wird dazu die Jahreszahl benötigt, könnte die Klasse LocalDate benutzet werden. LocalTime hingegen betrachtet nur Zeitangaben in Stunden, Minuten und Sekunden. LocalDateTime kombiniert, wie es der Name schon vermuten lässt, die Datumsangaben von LocalDate mit der Zeitangaben von LocalTime. Darüber hinaus kann für eine exakte Zeitangabe noch die Zeitzone hinzugenommen werden, abgebildet durch die Klasse ZonedDateTime. Im folgenden Beispiel wird aus einem LocalTime und aus einem LocalDate Objekt ein LocalDateTime Objekt erzeugt.

LocalTime time = LocalTime.of(12,12,12);
LocalDate date = LocalDate.of(2000, Month.JANUARY, 1);
LocalDateTime dateTime = LocalDateTime.of(date, time);

Die Ausgabe mit der toString()-Methode sieht dann wie folgt aus: „dateTime: 2000-01-01T12:12:12“. In diesem Zusammenhang sind die Enums DayOfWeek und Month praktisch einsetzbar. DayOfWeek enthält sieben Konstanten für die Wochentage, von Montag(1) bis Sonntag(7). Month besteht aus 12 Konstanten und weist den Monaten Januar(1) bis Dezember(12) entsprechende Werte zu. Die Verwendung dieser Konstanten, z. B. DayOfWeek.MONDAY oder Month.JANUARY, soll den Code in eine lesbarere Form bringen.

In der kontinuierlichen Zeitrechnung ist es möglich, eine Zeitspanne zwischen zwei Zeitpunkten mit Duration zu bestimmen. In der menschlichen Zeitrechnung geschieht dies ganz ähnlich mit der Klasse Period. So kann wie in folgendem Beispiel die Zeitspanne zwischen zwei LocalDate-Objekten berechnet werden.

LocalDate date1 = LocalDate.of(2000, Month.JANUARY, 1);
LocalDate date2 = LocalDate.of(2001, Month.APRIL, 5);
Period between = Period.between(date1, date2);

Die Ausgabe von between ergibt dann: „P1Y3M4D“, wobei das vorangestellte P für Period steht, Y steht für Year, M steht für Month und D steht analog für Day. Die Ausgabe repräsentiert also einen Zeitraum von einem Jahr, drei Monaten und vier Tagen.

Wem die Zeitangabe aus Jahr, Monat, Tag, Stunde, Minute und Sekunde noch zu unpräzise ist, der kann zusätzlich noch die Zeitzone per ZoneId angeben.

LocalDateTime dateTime = LocalDateTime.of(date, time);
ZoneId timeZone = ZoneId.of("Europe/Berlin");
ZonedDateTime zonedDateTime = ZonedDateTime.of(dateTime, timeZone);

In diesem Beispiel wird aus einem LocalDateTime-Objekt ein ZonedDateTime-Objekt erzeugt, indem ihm eine Zeitzone als ZoneId-Objekt hinzugefügt wird. Das ZoneOffset wird hier automatisch berechnet und beträgt „+01:00“, wie die Ausgabe der toString()-Methode zeigt: „2000-01-01T12:12:12+01:00[Europe/Berlin]“.

Außerdem existieren noch die Klassen OffsetDateTime und OffsetTime. OffsetDateTime besitzt verglichen mit ZonedDateTime keine ZoneId, bei OffsetTime fehlen zusätzlich noch die Datumsangaben wie Tag, Monat und Jahr.

Die Zeit fließt dahin

Alle oben vorgestellten Klassen besitzen eine ähnliche Methodenstruktur. Durch den „Fluent Programming Style“ lassen sich diese Methoden verketten, wodurch sich diese Kette flüssig lesen lassen soll. Die folgende Tabelle gibt die Präfixe der Methoden mit ihrer Bedeutung an.

Präfix Verwendung/Erklärung
of Erzeugt eine Instanz zum Validieren der Eingabeparameter, ohne Konvertierung
from Konvertiert die Eingabeparameter zu einer Instanz der Zielklasse; Information kann verloren gehen
get Gibt den Wert eines Feldes zurück
is Fragt den Zustand des Zielobjekts ab
with Gibt eine Kopie des Zielobjekts mit geändertem Wert zurück; Äquivalent zu „set“
plus Addiert einen Wert
minus Subtrahiert einen Wert
to Konvertiert das Objekt in einen anderen Typ
at Kombiniert das Objekt mit einem anderen Objekt

Diese Methoden geben immer einen Wert (außer dem null-Wert) zurück und nehmen keine null-Werte als Parameter an. Es wird immer eine Kopie des Objekts mit geänderten Werten erzeugt, wodurch die Objekte unveränderbar bzw. threadsicher sind. Das folgende Beispiel zeigt eine solche Verkettung von Methodenaufrufen:

DayOfWeek weekDay = ZonedDateTime.now().plusDays(10).minusMinutes(10).withSecond(10).getDayOfWeek();

Hier wird ein weekDay-Objekt erzeugt, indem ein ZonedDateTime-Objekt mit den aktuellen Werten erstellt wird, dann zehn Tage addiert werden, zehn Minuten subtrahiert, die Sekunden auf den Wert zehn gesetzt und abschließend der resultierende Wochentag abgefragt wird. So können teilweise ziemlich lange Ketten von Methodenaufrufen entstehen, je nach der Komplexität der Berechnung.

Parsen und Formatieren

Die Klasse DateTimeFormatter im java.time.format Package kann zum Formatieren der Ausgabe von Zeit- und Datumsangaben benutzt werden. Unter Verwendung eines eigenen Patterns kann so die Ausgabe nahezu beliebig angepasst werden.

LocalDateTime dateTime = LocalDateTime.of(2000, 1, 1, 20, 0, 0);
String pattern = " yyyy MMM dd HH mm ss ";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
String out = formatter.format(dateTime);

Die Ausgabe des Strings out besitzt dann folgende Form: „2000 Jan 01 20 00 00“. Analog dazu funktioniert auch die entgegengesetzte Richtung des Parsens von Datum- und Zeitangaben aus Strings.

String date = "2000 Jan 01 20 00 00";
String pattern = "yyyy MMM dd HH mm ss";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
LocalDateTime dateTime = LocalDateTime.parse(date, formatter);

Falls es beim Konvertieren von Werten zu Fehlern kommt, sollte sichergestellt sein, dass die entsprechenden DateTimeExceptions und DateTimeParseExceptions abgefangen werden. Außerdem muss nicht zwangsläufig ein eigens definiertes Pattern angeben werden, die DateTimeFormatter Klasse bietet bereits eine Vielzahl von vordefinierten, standardisierten Formattern an.

Damals und Heute

Wer kennt es nicht: Das neueste Release einer Technologie steht vor der Tür und die Vielzahl an neuen Features möchte unbedingt benutzt werden. Allerdings wurde in laufenden Projekten bereits intensiver Gebrauch von der Vorgängerversion gemacht. Da es natürlich wenig Sinn ergibt, vorhandenen Code neu zu schreiben, wurden im Fall der Java 8 Date and Time API die alten Date und Calendar Klassen mit Methoden versehen, um vorhandene Werte in Typen der neuen API und umgekehrt zu konvertieren. Es existiert leider kein 1:1 Mapping von alten zu neuen Klassen, allerdings lässt sich mit den vorhandenen Erweiterungen der Schritt zur neuen API mehr oder weniger elegant vollziehen. Nachfolgend ein paar Beispiele für die unterschiedlichsten Anwendungsfälle:

  • CalendartoInstant() – konvertiert ein Calendar Objekt in ein Instant Objekt
  • GregorianCalendar.toZonedDateTime() – konvertiert ein GregorianCalendar Objekt in ein ZonedDateTime Objekt
  • GregorianCalendar.from(ZonedDateTime) – erzeugt ein GregorianCalendar Objekt mit dem default locale aus einem ZonedDateTime Objekt
  • Date.from(Instant) – erzeugt ein Date Objekt aus einem Instant Objekt
  • Date.toInstant() – konvertiert ein Date Objekt zu einem Instant Objekt
  • TimeZone.toZoneId() – konvertiert ein TimeZone Objekt zu einem ZoneId Objekt

Dies ist nur ein Auszug aus allen Methoden, die der Konvertierung zwischen den Objekten der java.util Klassen und der java.time Klassen dienen. Für eine komplette Übersicht sei hier auf die entsprechende Dokumentation der API verwiesen.

Fazit

Prinzipiell kann die neue Date and Time API von Java 8 als gelungen betrachtet werden. Zwar erfordert es etwas Aufwand, sich mit den neuen Konzepten und Abstraktionen vertraut zu machen, wurde diese Hürde allerdings genommen, steht einem eine umfangreiche API für Zeit- und Datumsangaben zur Verfügung. Die API enthält noch weitere Neuerungen, die an dieser Stelle nicht alle im Detail betrachtet werden können. Es gibt z. B. noch das java.time.temporal Package, welchem hier weniger Beachtung geschenkt wurde. Ob einem der „Fluent Programming Style“ gefällt oder nicht – unbestritten ist auf jeden Fall der Vorteil der Threadsicherheit für parallele Anwendungen. Wer sich partout gegen die Neuerungen sträubt, kann übrigens auch weiterhin die alten java.util Klassen verwenden, diese existieren parallel weiter. Somit wird die alte API nicht vollständig abgelöst und es gibt momentan die Auswahl zwischen der alten und der neuen API.

 

Als weiterführende Literatur zu diesem Thema sei auf das offizielle Tutorial von Oracle verwiesen.

Für eine komplette Übersicht ist die entsprechende Dokumentation der API empfehlenswert.

Das könnte Sie auch interessieren:
Java 8: Die wichtigsten Neuerungen für Entwickler