15.8 Date-Time-API
Seit Java 8 gibt es das Paket java.time, das alle bisherigen Java-Typen rund um Datum- und Zeitverarbeitung überflüssig macht. Mit anderen Worten: Mit den neueren Typen lassen sich Date, Calendar, GregorianCalendar, TimeZone usw. streichen und ersetzen. Natürlich gibt es Adapter zwischen den APIs, doch gibt es nur noch sehr wenige zwingende Gründe, heute bei neuen Programmen auf die älteren Typen zurückzugreifen – ein Grund ist natürlich die heilige Kompatibilität.
Die neue API basiert auf dem standardisierten Kalendersystem von ISO-8601, und das deckt ab, wie ein Datum, wie Zeit, Datum und Zeit, UTC, Zeitintervalle (Dauer/Zeitspanne) und Zeitzonen repräsentiert werden. Die Implementierung basiert auf dem gregorianischen Kalender, wobei auch andere Kalendertypen denkbar sind. Javas Kalendersystem greift auf andere Standards bzw. Implementierungen zurück, unter anderem auf das Unicode Common Locale Data Repository (CLDR) zur Lokalisierung von Wochentagen oder auf die Time-Zone Database (TZDB), die alle Zeitzonenwechsel seit 1970 dokumentiert. In Java nutzen die XML-APIs schon länger ISO-8601-Kalender, denn Schema-Dateien nutzen einen XMLGregorianCalendar, und selbst für Dauern gibt es einen eigenen Typ, Duration.
[»] Geschichte
Über die alten Datumsklassen meckert die Java-Community seit über zehn Jahren – nicht ganz zu Unrecht. So hat ein Date zum Beispiel einen Datums- sowie einen Zeitanteil, Kalender fehlen, die Sommerzeitumstellung verschiedener Länder wird nicht korrekt behandelt und es gibt weitere Schwächen.[ 244 ](Der Quellcode stammt von IBM. Trifft Oracle jetzt die Schuld, weil Sun die Implementierung damals übernahm? Siehe zu den Kritiken auch http://tutego.de/go/dategotchas. ) Daher geht die Entwicklung der Date-Time-API lange zurück und basiert auf Ideen von Joda-Time (http://joda-time.sourceforge.net/), einer populären quelloffenen Bibliothek. Spezifiziert im JSR 310 (eingereicht am 30. Jan 2007)[ 245 ](http://jcp.org/en/jsr/detail?id=310) und angedacht für Java 7 (das vier Jahre später, im Juli 2011, erschien), wurde die API erst in Java 8 Teil der Java SE. Für Java 1.7 gibt es einen Back-Port (http://github.com/ThreeTen/threetenbp), um später leicht die Codebasis auf Java 8 zu migrieren, der durchaus interessant ist.
Erster Überblick
Die zentralen temporalen Typen aus der Date-Time-API sind schnell dokumentiert:
Typ | Beschreibung | Feld(er) |
---|---|---|
LocalDate | Repräsentiert ein übliches Datum. | Jahr, Monat, Tag |
LocalTime | Repräsentiert eine übliche Zeit. | Stunden, Minuten, Sekunden, Nanosekunden |
LocalDateTime | Kombination aus Datum und Zeit | Jahr, Monat, Tag, Stunden, Minuten, Sekunden, Nanosekunden |
Period | Dauer zwischen zwei LocalDates | Jahr, Monat, Tag |
Year | Nur Jahr | Jahr |
Month | Nur Monat | Monat |
MonthDay | Nur Monat und Tag | Monat, Tag |
OffsetTime | Zeit mit Zeitzone | Stunden, Minuten, Sekunden, Nanosekunden, Zonen-Offset |
OffsetDateTime | Datum und Zeit mit Zeitzone als UTC-Offset | Jahr, Monat, Tag, Stunden, Minuten, Sekunden, Nanosekunden, Zonen-Offset |
ZonedDateTime | Datum und Zeit mit Zeitzone als ID und Offset | Jahr, Monat, Tag, Stunden, Minuten, Sekunden, Nanosekunden, Zonen-Info |
Instant | Zeitpunkt (fortlaufende Maschinenzeit) | Nanosekunden |
Duration | Zeitintervall zwischen zwei Instants | Sekunden/Nanosekunden |
15.8.1 Menschenzeit und Maschinenzeit
Datum und Zeit, die wir als Menschen in Einheiten wie Tagen und Minuten verstehen, nennen wir Menschenzeit (engl. human time), die fortlaufende Zeit des Computers, die eine Auflösung im Nanosekundenbereich hat, Maschinenzeit. Die Maschinenzeit startet dabei von einer Zeit, die wir Epoche nennen, z. B. die Unix-Epoche.
Aus Abschnitt 7.2.5, »Sehen Kinder alles? Die Sichtbarkeit protected«, lässt sich gut ablesen, dass die meisten Klassen für uns Menschen gemacht sind und dass sich nur Instant/Duration auf die Maschinenzeit bezieht. LocalDate, LocalTime und LocalDateTime repräsentieren Menschenzeit ohne Bezug zu einer Zeitzone, ZonedDateTime mit Bezug auf eine Zeitzone. Bei der Auswahl der richtigen Zeitklassen für eine Aufgabenstellung ist natürlich die erste Überlegung, ob die Menschenzeit oder die Maschinenzeit repräsentiert werden soll. Dann folgen die Fragen, was genau für Felder nötig sind und ob eine Zeitzone relevant ist oder nicht. Soll zum Beispiel die Ausführungszeit gemessen werden, ist es unnötig, zu wissen, an welchem Datum die Messung begann und endet; hier ist Duration korrekt, nicht Period.
[zB] Beispiel
LocalDate now = LocalDate.now();
System.out.println( now ); // 2018-03-09
System.out.printf( "%d. %s %d%n",
now.getDayOfMonth(),
now.getMonth(), now.getYear() ); // 9. MARCH 2018
LocalDate bdayMLKing = LocalDate.of( 1929, Month.JANUARY, 15 );
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate( FormatStyle.MEDIUM );
System.out.println( bdayMLKing.format( formatter ) ); // 15. Januar 1929
Die Methode getMonth() auf einem LocalDate liefert als Ergebnis ein java.time.Month-Objekt, und das sind Aufzählungen. Die toString()-Repräsentation liefert die Konstante in Großbuchstaben.
Alle Klassen basieren standardmäßig auf dem ISO-System. Andere Kalendersysteme, wie der japanische Kalender, werden über Typen aus java.time.chrono erzeugt, und natürlich sind auch ganz neue Systeme möglich.
[zB] Beispiel
ChronoLocalDate now = JapaneseChronology.INSTANCE.dateNow();
System.out.println( now ); // Japanese Heisei 19-10-23
Paketübersicht
Die Typen der Date-Time-API verteilen sich auf verschiedene Pakete:
java.time: Enthält die Standardklassen wie LocalTime und Instant. Alle Typen basieren auf dem Kalendersystem ISO-8601, das landläufig unter »gregorianischer Kalender« bekannt ist. Dieser wird zum sogenannten Proleptic Gregorian Calendar erweitert. Das ist ein gregorianischer Kalender, der auch für die Zeit vor 1582 (der Einführung dieses Kalenders) gültig ist, damit eine konsistente Zeitlinie entsteht.
java.time.chrono: Hier befinden sich vorgefertigte alternative (also Nicht-ISO-)Kalendersysteme, wie der japanische Kalender, der Thai-Buddhist-Kalender, der islamische Kalender und ein paar weitere.
java.time.format: Klassen zum Formatieren und Parsen von Datum- und Zeit, wie der genannte DateTimeFormatter
java.time.zone: unterstützende Klassen für Zeitzonen, etwa ZonedDateTime
java.time.temporal: tiefer liegende API, die Zugriff und Modifikation einzelner Felder eines Datums/Zeitwerts erlaubt
Designprinzipen
Bevor wir uns mit den einzelnen Klassen auseinandersetzen, wollen wir uns mit den Designprinzipien beschäftigen, denn alle Typen der Date-Time-API folgen wiederkehrenden Mustern. Die erste und wichtigste Eigenschaft ist, dass alle Objekte immutable sind, also nicht veränderbar. Das ist bei der »alten« API anders: Date und die Calendar-Klassen sind veränderbar, mit teils verheerenden Folgen. Denn werden diese Objekte herumgereicht und verändert, kann es zu unkalkulierbaren Seiteneffekten kommen. Die Klassen der neuen Date-Time-API sind immutable, und so stehen die Datum/Zeit-Klassen wie LocalTime oder Instant den veränderbaren Typen wie Date oder Calendar gegenüber. Alle Methoden, die nach Änderung aussehen, erzeugen neue Objekte mit den gewünschten Änderungen. Seiteneffekte bleiben also aus, und alle Typen sind threadsicher.
Unveränderbarkeit ist eine Designeigenschaft wie auch die Tatsache, dass null nicht als Argument erlaubt wird. In der Java-API wird oftmals null akzeptiert, weil es etwas Optionales ausdrückt, doch die Date-Time-API straft dies in der Regel mit einer NullPointerExcpetion. Dass null nicht als Argument und nicht als Rückgabe im Einsatz ist, kommt einer weiteren Eigenschaft zugute: Die API gestattet »flüssige« Ausdrücke, also kaskadierte Aufrufe, da viele Methoden die this-Referenz zurückgeben, so wie das auch von StringBuilder bekannt ist.
Zu diesen eher technischen Eigenschaften kommt die konsistente Namensgebung hinzu, die sich von der Namensgebung der bekannten JavaBeans absetzt. So gibt es keine Konstruktoren und keine Setter (das brauchen die immutablen Klassen nicht), sondern Muster, die viele der Typen aus der Date-Time-API einhalten:
Klassen-/Exemplarmethode | Grundsätzliche Bedeutung | |
---|---|---|
now() | Statisch | Liefert ein Objekt mit aktueller Zeit/ aktuellem Datum. |
ofXXX() | Statisch | Erzeugt neue Objekte. |
fromXXX() | Statisch | Erzeugt neue Objekte aus anderen Repräsentationen. |
parseXXX() | Statisch | Erzeugt ein neues Objekt aus einer String- Repräsentation. |
format() | Exemplar | Formatiert und liefert einen String. |
getXXX() | Exemplar | Liefert Felder eines Objekts. |
isXXX() | Exemplar | Fragt den Status eines Objekts ab. |
withXXX() | Exemplar | Liefert eine Kopie des Objekts mit einer geänderten Eigenschaft. |
plusXXX() | Exemplar | Liefert eine Kopie des Objekts mit einer aufsummierten Eigenschaft. |
minusXXX() | Exemplar | Liefert eine Kopie des Objekts mit einer reduzierten Eigenschaft. |
toXXX() | Exemplar | Konvertiert ein Objekt in einen neuen Typ. |
atXXX() | Exemplar | Kombiniert dieses Objekt mit einem anderen Objekt. |
XXXInto() | Exemplar | Kombiniert ein eigenes Objekt mit einem anderen Zielobjekt. |
Die Methode now() haben wir schon in den ersten Beispielen verwendet, sie liefert zum Beispiel das aktuelle Datum. Weitere Erzeugermethoden sind die mit dem Präfix of, from oder with; Konstruktoren gibt es nicht. withXXX()-Methoden nehmen die Rolle der Setter ein.
15.8.2 Die Datumsklasse LocalDate
Ein Datum (ohne Zeitzone) repräsentiert die Klasse LocalDate. Damit lässt sich zum Beispiel ein Geburtsdatum repräsentieren.
Ein temporales Objekt kann über die statischen of(…)-Fabrikmethoden aufgebaut und über ofInstant(Instant instant, ZoneId zone) oder von einem anderen temporalen Objekt abgeleitet werden. Interessant sind die Methoden, die mit einem TemporalAdjuster arbeiten.
[zB] Beispiel
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with( TemporalAdjusters.next( DayOfWeek.SATURDAY ) );
System.out.printf( "Heute ist der %s, und frei ist am Samstag, den %s",
today, nextMonday );
Mit den Objekten in der Hand können wir diverse Getter nutzen und einzelne Felder erfragen, etwa getDayOfMonth(), getDayOfYear() (liefern int) oder getDayOfWeek(), das eine Aufzählung vom Typ DayOfWeek liefert, und getMonth(), das eine Aufzählung vom Typ Month liefert. Weiterhin gibt es long toEpochDay() und long toEpochSecond(LocalTime time, ZoneOffset offset).
Dazu kommen Methoden, die mit minusXXX(…) oder plusXXX(…) neue LocalDate-Objekte liefern, wenn zum Beispiel mit minusYear(long yearsToSubtract) eine Anzahl Jahre zurückgelaufen werden soll. Durch die Negation des Vorzeichens kann auch die jeweils entgegengesetzte Methode genutzt werden, sprich, LocalDate.now().minusMonths(1) kommt zum gleichen Ergebnis wie LocalDate.now().plusMonths(-1). Die withXXX(…)-Methoden belegen ein Feld neu und liefern ein modifiziertes neues LocalDate-Objekt.
Von einem LocaleDate lassen sich andere temporale Objekte bilden; atTime(…) etwa liefert LocalDateTime-Objekte, bei denen gewisse Zeitfelder belegt sind. atTime(int hour, int minute) ist so ein Beispiel. Mit until(…) lässt sich eine Zeitdauer vom Typ Period liefern. Interessant sind zwei Methoden, die einen Strom von LocalDate-Objekten bis zu einem Endpunkt liefern:
Stream<LocalDate> datesUntil( LocalDate endExclusive )
Stream<LocalDate> datesUntil( LocalDate endExclusive, Period step )