Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
NEWSLETTER 
CONTACT 
Effective Java

Effective Java
Java 8
Das Date/Time API
 

Java Magazin, Januar 2015
Klaus Kreft & Angelika Langer

Dies ist die Überarbeitung eines Manuskripts für einen Artikel, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

Wir setzen unsere Reihe mit Java 8 Neuerungen diesmal mit den neuen Abstraktionen, um Zeit- und Datumsangaben auszudrücken, fort.  Dieses neue Date/Time-API löst alte Abstraktionen wie  Date Calendar DateFormat TimeZone , etc. ab.
 

Wozu überhaupt neue Date-/Time-Abstraktionen?

Die Klasse  java.util.Date gibt es im JDK seit Java 1.0 und sie gehört zu den "interessantesten" Klassen im JDK überhaupt.  Es geht damit los, dass die Argumente für die Erzeugung eines  Date -Objekt alles andere als intuitiv erfassbar sind.  Mit

Date date = new Date(2007,11,13,16,40);

wird nicht etwa das Datum "13. November 2007, 16:40 Uhr" ausgedrückt, wie man vielleicht erwarten könnte.  Vielmehr bezeichnet es einen Punkt in ferner Zukunft, nämlich den 13. Dezember im Jahre 3907 um 16:40 Uhr.  Den "13. November 2007, 16:40 Uhr" muss man vielmehr so erzeugen:

Date date = new Date(2007 -1900 ,11 -1 ,13,16,40);

Dieses  Date -Klasse kann man nur begreifen, wenn man mal in C programmiert hat und dort ähnliche Abstraktionen verwendet hat, die die Monate mit 0 beginnend numerieren und deren Zeitrechnung im Jahre 1900 beginnen.

Der Konstruktor ist aber nur eine der vielen Seltsamkeiten an dieser Klasse.  Man kann keine Zeitzone zuordnen, die Abstraktion ist nicht internationalisierbar, und überhaupt … warum heißt die Klasse  Date , wenn sie in Wirklichkeit nicht nur ein Datum, sondern auch eine Zeitangabe enthält?  Die Klasse  Date wurde dann rasch in Java 1.1 durch die Klasse  Calendar ersetzt, aber auch  Calendar lässt Wünsche offen.  Hinzu kommt, dass nicht eine einzige der alten Date-Time-Abstraktionen threadsicher ist, was wesentlich daran liegt, dass es alles veränderliche Typen sind.

Über die Zeit ist unabhängig vom JDK als Alternative zu  Date / Calendar die Open Source Bibliothek "Joda Time" entstanden (siehe / JODA /), die deutlich klarere Abstraktionen für Datum und Zeit anbietet.  Inspiriert von Joda Time hat Stephen Colebourne, der Project Lead von Joda Time, ein Date & Time API für den JDK vorgeschlagen (siehe / JSR310 /).  Dieses Date & Time API wurde mit Java 8 als Bestandteil des JDK frei gegeben und ist ein wunderbares Beispiel für elegantes und intelligentes API-Design. 

Die Design-Prinzipien des Date & Time API

Es folgt einer Reihe von Design-Prinzipien:

- Nahezu alle Abstraktionen im Date & Time API sind unveränderlich und damit threadsicher.  Außerdem lassen sich unveränderliche Daten problemlos cachen; das ist auch noch günstig für die Performance auf Mulitcore-Architekturen.

- Alle Methoden des Date & Time APIs sind wohl definiert: sie sind klar, verständlich und erwartungskonform.  Beispielsweise werden Monate als Enum-Typ ausgedrückt - was ja auch naheliegend ist- der November heißt dann auch  Month.NOVEMBER (und nicht  10 wie bei der alten  Date -Klasse).

- Das Date & Time API unterstützt den "Fluent Programming Style", den auch andere Java-8-Schnittstellen wie die Streams oder das  CompletableFuture anbieten.

- Das Date & Time API ist erweiterbar.  Es hat Low-Level-Abstraktionen, die es erlauben, eigene Kalendersysteme zu implementieren.  Die Entwicker des Date & Time API sind davon ausgegangen, dass sie unmöglich alle Anforderungen kennen können und haben deshalb die Erweiterbarkeit bewusst eingeplant.

Schauen wir uns also an, wie das neue Date & Time API aussieht.

Was ist "Zeit"?

"Zeit" kann vieles bedeuten: es kann eine Zeitachse sein, ein Zeitpunkt, eine Zeitspanne, ein Zeitinterval, eine Zeitdauer, die vierte Dimension, …  Wie man am Beispiel der alten  Date / Calendar -Klassen gesehen hat (wo ein  Date -Objekt eigentlich eine Date-Time-Objekt ist), muss man zunächst einmal klären, was man eigentlich meint und ausdrücken will.
 
 

Im Wesentlichen gibt es zwei Arten, um Zeit auszudrücken:

- kontinuierlich ; als Zähler, der fortwährend inkrementiert wird.  Das ist die Art, wie Maschinen Zeit erfassen.

- menschlich ; als Ansammlung von Feldern wie Jahr, Monat, Tag, Stunde, Minute, Sekunde.  Das ist die Art, wie wir Menschen Zeit ausdrücken und zählen.

- Für diese beiden Arten von  Zeit gibt es im Date & Time API verschiedene Abstraktionen.  Beginnen wir mit der kontinuierlichen Zeit.

Kontinuierliche Zeit

Die kontinuierliche Zeit wird beschrieben durch eine Zeitachse mit einem Ursprung.  Der Ursprung der Zeitachse wird als "epoch" bezeichnet.  Ein Beispiel für den Ursprung einer Zeitachse ist "Mitternacht am 1.1.1970 Greenwich-Zeit (GMT)"; das ist der willkürlich festgesetzte Beginn der Zeitzählung, wie er in Unix/POSIX und auch in Java verwendet wird.  Die Methode  System.currentTimeMillis() liefert beispielsweise Millisekunden seit "midnight 1-1-1970 GMT".

Ein Punkt auf dieser Zeitachse ist ein Moment im Zeitkontinuum.  Er wird als "instant" bezeichnet und im neuen Date & Time API durch die Klasse  Instant im Package  java.time repräsentiert. Ein Beispiel für einen Zeitpunkt ist "53628746263276 Nanosekunden nach dem Beginn der Zeitrechnung".  In Java werden Zeitpunkte in Nanosekunden-Genauigkeit erfasst im Bereich von  Instant.MIN bis  Instant.MAX .  Intern dargestellt werden sie als Paar von einem  long -Wert (die Entfernung vom Ursprung in Sekunden) plus einem  int -Wert (die Nanosekunden einer Sekunde, d.h. ein Wert zwischen 0 und 999.999.999).

Eine Menge an Zeit wird als "duration" bezeichnet.  Beispiele für eine solche Zeitdauer sind "5 Minuten" oder "358753871581 Nanosekunden".  Die Zeitdauer ist nicht an einen bestimmten Punkt auf der Zeitachse gebunden.  Sie ist gerichtet und kann sowohl positiv (in die Zukunft gerichtet) oder negativ (in die Vergangenheit gerichtet) sein.

Die Zeitdauer wird im neuen Date & Time API durch die Klasse  Duration im Package  java.time repräsentiert.

Die kontinuierliche Zeit wird in erste Linie für Berechnungen verwendet.  Zum Beispiel kann man  Instant und  Duration für die Zeitmessung in einem Benchmark verwenden:
 
 

long benchmark(int loopSize, Runnable algorithm) {

   Instant start =  Instant.now ();

  for (int i=0; i<loopSize; i++)

    algorithm.run();

  Instant end = Instant.now();

   Duration timeElapsed =  Duration.between (start, end);

  long millis = timeElapsed. toMillis ();

  return millis;

}
 
 

Da das neue Date & Time API verständlich definiert ist, dürfte das obige Code-Beispiel wohl selbsterklärend sein:   Instant.now() liefert den aktuellen Augenblick in Nanosekunden.   Duration.between() liefert die Zeitdauer zwischen den beiden Zeitpunkten.   Duration.toMillis() konvertiert die Nanosekunden in Millisekunden.

Die Klassen  Instant und  Duration haben zahlreiche arithmetische Methoden wie  plus() minus() multipliedBy() dividedBy() negated() , etc.  Außerdem kann man  Duration - und  Instant -Objekte vergleichen, z.B. mit  compareTo() und  equals() .  Man könnte beispielsweise damit ausrechnen, ob bei einem Benchmark eine Alternative mehr als doppelt so schnell ist wie eine andere:
 
 

Duration timeElapsed1 = Duration.between(start1, end1);

Duration timeElapsed2 = Duration.between(start2, end2);
 
 

boolean twiceAsFast // calculated with durations

= timeElapsed1.multiplied(2).minus(timeElapsed2).isNegative();
 
 

boolean twiceAsFast // calculated with nanos

= timeElapsed1.toNanos() * 2 < timeElapsed2.toNanos();
 
 

Die Berechnung auf Basis von Nanosekunden ist sicherlich einfacher und völlig ausreichend, solange nicht der gesamte Wertebereich der Zeitpunkte benötigt wird.  In Nanosekunden lassen sich immerhin Zeitspannen von fast 300 Jahren Länge ausdrücken.  Nur wenn Nanosekunden-Genauigkeit und lange Zeiträume benötigt werden, die zu einem Nanosekunden-Overflow führen würden, dann muss mit Hilfe der  Duration -Methoden Arithmetik betrieben werden.

Die Klassen  Instant und  Duration , wie übrigens fast alle Typen im neuen Date & Time API,  sind unveränderliche Typen.  Deshalb geben scheinbar verändernde Instanz-Methoden neue Objekte zurück anstatt existierende Objekte zu modifizieren.

Menschliche Zeit

Die für Menschen verständlichere Repräsentation von Datum und Zeit wird im neuen Date & Time API unterteilt in Date/Time-Abstraktionen mit und ohne Zeitzone. 
Ein Beispiel für einen Zeitpunkt mit Zeitzone ist "July 16, 1969, 09:32:00 EDT"; das war der Start der Apollo-11-Rakete von Cape Canaveral in Florida. Inklusive der Zeitzone EDT (Eastern Daylight Time) bezeichnet ein solcher Zeitpunkt einen exakten Punkt auf der Zeitachse.  Im neuen Date & Time API wird er durch die Klasse  ZonedDateTime im Package  java.time ausgedrückt.
Daneben gibt es Zeitpunkte ohne Zeitzone.  Beispiele sind Geburtstage wie der  18. Juli 1918 (Geburtstag von Nelson Mandela) oder der  28. April 1986 (der Tag der Chernobyl-Katastrophe).  Da sowohl die Uhrzeit als auch die Zeitzone fehlt, beschreibt ein solcher Zeitpunkt keinen präzisen Punkt auf der Zeitachse.  Für viele praktische Probleme ist ein solcher unpräziser Zeitpunkt völlig ausreichend.  Im neuen Date & Time API werden solche unpräzisen Zeitpunkte durch die Klassen  Local DateTime LocalDate und  LocalTime im Package  java.time ausgedrückt.
Sehen wir uns zunächst die Abstraktionen ohne Zeitzone an.

LocalDateTime - Zeitpunkte ohne Zeitzone

Ein  LocalDate ist ein Datum bestehend aus Jahr, Monat und Tag.  Man erzeugt es mit Hilfe von Factory-Methoden der Klasse  LocalDate .  Hier ein paar Beispiele:
 
 

LocalDate today             = LocalDate. now ();

LocalDate chernobylDisaster = LocalDate. of (1986, 4, 28);

LocalDate fukushimaDisaster = LocalDate. of (2011, Month.MARCH ,11);
 
 

Als Hilfe stehen übrigens mehrere Enum-Typen zur Verfügung:

Month := für die Monate ( JANUARY , FEBRUARY , ...)

DayOfWeek := für die Wochentage ( MONDAY , TUESDAY , ...)

ChronoUnit := als Zeiteinheit ( NANOS MICROS , ...,  DAYS , ...,  CENTURIES MILLENNIA FOREVER )

Auf diese Weise muss man einen Monat nicht numerisch als  4 ausdrücken, sondern man kann ihn auch als  Month.APRIL spezifizieren.

Aus einem  LocalDate können neue  LocalDate s berechnet werden.  Hier einige Beispiele:
 
 

LocalDate xmas = LocalDate.of(LocalDate.now(). getYear (),12,24);

LocalDate nextXmas = xmas. plusYears (1);

LocalDate xmasInTwoYears = xmas. plus (2,ChronoUnit.YEARS);

System.out.println("This year's Christmas Eve is on a "+ xmas. getDayOfWeek ());
 
 

Die resultierende Ausgabe auf  System.out könnte so aussehen:
 
 

This year's Christmas Eve is on a WEDNESDAY.
 
 

Wir erzeugen ein  LocalDate -Objekt für den Heiligen Abend im laufenden Jahr; das aktuelle Jahr wird mit  LocalDate.now().getYear() bestimmt.  Mit  plusYear(1) bekommen wir den Heiligen Abend im nächsten Jahr und mit  plus(2, ChronoUnit.YEARS ) den Heiligen Abend in 2 Jahren.  Die Methode  getDayOfWeek() liefert den Wochentag.  Natürlich gibt es auch entsprechende  minus() -Methoden.

Die Klasse  LocalDate ist, wie übrigens fast alle Typen im neuen Date & Time API, ein unveränderlicher Typ und alle Methoden geben jeweils neue Objekte zurück anstatt existierende Objekte zu modifizieren.  Wenn man ein LocalDate "ändern" will, dann erzeugt man ein neues, dass die "Änderung" widerspiegelt.  Die Methoden dafür heißen beispielsweise  withDayOfMonth() withDay OfYear() withMonth()  oder  withYear() .  Hier ist ein Beispiel, in dem der nächste 10. des Monats bestimmt wird, d.h. der Termin für die nächste Umsatzsteuervoranmeldung:
 
 

LocalDate today = LocalDate.now();

LocalDate nextTaxDeadline

= today. withDayOfMonth (10)

       . plusMonths ((today.getDayOfMonth()>10)?1:0);
 
 

Wir nehmen das Datum von heute, "setzen" den Tag auf den 10. und "addieren" entweder 0 oder 1 Monat, je nachdem ob das heutige Datum einen Tag größer oder kleiner-gleich dem 10. hat.  Beim "setzen" und "addieren" werden jeweils neue  LocalDate -Objekte erzeugt; deshalb wird der Returnwert dieser Methoden entweder einer neuen  LocalDate -Variablen zugewiesen oder für den Aufruf einer weiteren Methode genutzt.  Man sieht an diesem Beispiel auch den Fluent-Programming-Style, bei dem auf das Ergebnis der vorangegangenen Operation ( withDayOfMonth() ) die nächste Operation ( plusMonths() ) angewandt wird, so dass sich eine Kette von Operationen ergibt.

Period - Zeitspanne

Die Zeit zwischen zwei Daten wird mit der Klasse  Period ausgedrückt.  Sie kann in Jahren, Monaten oder Tagen angegeben werden.  Die Zeitspanne von heute bis zum letzten oder nächsten Jahresanfang wäre eine solche  Period .  Man könnte sie so berechnen:
 
 

Period daysUntilNewYear = today. until (newYearsDayThisYear);

Period daysSinceLastYear = Period. between (today,newYearsDayThisYear);
 
 

Die  Period ist für die  LocalDate s, was die  Duration für die  Instant s ist: die Menge an Zeit zwischen zwei Zeitpunkten.  Allerdings ist die Maßeinheit anders (Jahre, Monate, Tage für  Period und Nanosekunden für  Duration ). 

Beide Klassen haben übrigens ein gemeinsames Super-Interface, nämlich das Interface  TemporalAmount im Package  java.temporal .  Ein  TemporalAmount betrachtet eine Zeitspanne im Prinzip als eine Liste von Paaren bestehend aus einer Zeiteinheit (vom Typ  TemporalUnit ) und einem Wert (vom Typ  long ).  Ein TemporalAmount ist also zum Beispiel so etwas wie "7 Jahre, 3 Monate und 5 Tage".  Die Zeiteinheiten bekommt man mit der Methode  getUnits() und den jeweiligen  long -Wert mit  get(TemporalUnit) .  Hier ist ein Beispiel, wie man einen  Temporal Amount auslesen kann:
 
 

String toString( TemporalAmount period) {

  StringBuilder buf = new StringBuilder();

  for (TemporalUnit u : period. getUnits ()) {

    if (period.get(u)!=0)

       buf.append(Long.toString(period. get(u) )+" "+u+" ");

  }

  return buf.toString();

}
 
 

Mit dieser Method kann man einen  Temporal Amount in eine  String -Darstellung verwandeln.  Hier benutzen wir die Methode:
 
 

TemporalAmount daysUntilXmas = Period.between(today,nextXmasEve);

System.out.println(toString(daysUntilXmas));
 
 

Dann kommt beispielsweise heraus:
 
 

3 Months 30 Days
 
 

Selbstverständlich kann man  LocalDate -Objekte entlang der Zeitachse einordnen mit den Methoden  isBefore() und  isAfter() .  Außerdem kann man prüfen, ob die Jahresangabe ein Schaltjahr bezeichnet mit der Methode  isLeapYear() .

Neben  LocalDate , das aus Jahr, Monat und Tag besteht, kann man auch partielle Datumsangaben ausdrücken, nämlich durch die Klassen  MonthDay YearMonth und  Year .  Hier ist ein Beispiel:
 
 

MonthDay laDiadaDeCatalunya = MonthDay.of(Month.SEPTEMBER,11);

LocalDate siegeOfBarcelona = laDiadaDeCatalunya. atYear (1714);
 
 

Der 11. September ist der katalonische Nationalfeiertag.  Das ist eine Datumsangabe ohne spezifische Jahresangabe.  Man kann daraus ein komplettes Datum machen, indem man die Jahresangabe mit  atYear() hinzufügt.  Analog kann man aus einem vollständigen Datum ein unvollständiges machen:
 
 

LocalDate battleOfPuebla = LocalDate.of(5,Month.MAY,1862);

MonthDay elCincoDeMayo = MonthDay. from (battleOfPuebla);
 
 

Aus dem historischen Datum 5. Mai 1862 wird der 5. Mai, ein mexikanischer Feiertag.

TemporalAdjuster  - Komplexe Datumsberechnungen

Für komplexere Berechnungen mit Datumsangaben gibt es sogenannte "adjuster".  Damit kann man Dinge bestimmen wie den "3. Freitag des Monats" oder den "letzten Tag des vergangenen Monats".  Hier ist ein Beispiel, in dem der erste Samstag eines bestimmten Monats ermittelt wird:
 
 

LocalDate.of(2015,Month.JANUARY,1)

         .with(TemporalAdjusters.firstInMonth(DayOfWeek.SATURDAY));
 
 

Das Datum 1.1.2015 wird mit dem Adjuster  firstInMonth(DayOfWeek.SATURDAY) angepasst.  Heraus kommt das  LocalDate mit dem Datum des ersten Samstag im Januar 2015.  Der  firstInMonth -Adjuster ist einer von mehreren vordefinierten Adjustern; man findet sie in der Klasse  TermporalAdjusters im Package  java.time.temporal

Man kann Adjuster aber auch selber definieren.   Dazu muss man das Interface  TemporalAdjuster im Package  java.time.temporal implementieren.  Das Interface sieht so aus:
 
 

public interface TemporalAdjuster {

Temporal adjustInto(Temporal input);

}
 
 

Dabei ist  Temporal ein Interface, das von den Klassen  Instant LocalDate , usw. implementiert wird.  Hier ist ein Beispiel für einen selbstdefinierten Adjuster; er bestimmt den Anfang des kommenden Wochenendes:
 
 

LocalDate nextWeekend(LocalDate date) {

   TemporalAdjuster nextWeekendAdjuster = (Temporal d) -> {

    LocalDate result = (LocalDate) d;

    while (result.getDayOfWeek().getValue() <= 5) {

      result = result.plusDays(1);

    }

    return result;

  };

  return date.with(nextWeekendAdjuster);

}
 
 

Zunächst bauen wir uns einen Adjuster, indem wir das  TemporalAdjuster -Interface mit Hilfe einer Lambda Expression implementieren. Der Adjuster holt sich mit  getDayOfWeek().getValue() aus dem spezifizierten  LocalDate den Wochentag; solange der Wochentag Montag bis Freitag (also  <= 5 ) ist, wird einen Tag addiert.  Am Ende liefert der Adjuster das  LocalDate -Objekt für den nächsten Samstag.  Den selbstdefinierten Adjuster wenden wir anschließend mit  with() auf das spezifizierte Datum an.

Wir haben nun am Beispiel von  LocalDate gesehen, wie man Datumsobjekte erzeugt und  plus() minus() with() , usw. anpasst.  Die Abstraktionen  LocalTime und  LocalDateTime funktionieren ganz analog. 

ZonedDateTime - Zeitpunkte mit Zeitzone

Zeitangaben mit Zeitzone beschreiben präzise Zeitpunkte auf der Zeitachse.  Sie werden im neuen Date & Time API durch die Klasse  ZonedDateTime im Package  java.time repräsentiert.  Anders als bei den Zeitangaben ohne Zeitzone, wo es  LocalDateTime LocalDate und  LocalTime gibt, wird die Zeitangabe mit Zeitzone allein durch die Klasse  ZonedDateTime ausgedrückt; es gibt kein  ZonedDate - oder  ZonedTime -Klasse.

Eine Zeitangabe mit Zeitzone unterscheidet sich von einer Zeitangabe ohne Zeitzone dadurch, dass sie von Ort zu Ort unterschiedlich ausgedrückt wird.  Die Regeln dafür sind mehr oder weniger willkürlich, dann sie werden von der jeweiligen lokalen Administration nach Belieben festgelegt und geändert.  Die Festlegungen betreffen u. a. die Zuordnung eines Orts zu einer Zeitzone und den Wechsel zwischen Sommer- und Winterzeit.  Die Zeitzonen-Regeln werden von der Organisation IANA (Internet Assigned Numbers Authority) in einer Datenbank gesammelt (siehe https://www.iana.org/time-zones ).  Die Zeitzonen-Abstraktionen in Java benutzen diese IANA-Datenbank. 

Eine Zeitzone ist im neuen Date & Time API repräsentiert durch die Klassen  ZoneId und  ZoneOffset im Package  java.time und  ZoneRules im Package  java.time.zone

- Die  ZoneId bezeichnet die geographische Region.  Es ist ein Name für eine Zeitzone; ein Beispiel ist "Europe/Berlin". 

- Der  ZoneOffset gibt die Differenz zur Greenwich-Zeit (UTC) an; der Offset wird in Stunden zwischen +14:00 und -12:00 angegeben.  Die Zeitzone "Europe/Berlin" beispielsweise hat im Winter den Offset +01:00 und im Sommer den Offset +02:00.

- Die ZoneRules beschreiben wie und wann sich der Offset ändert.  Ein Beispiel sind die Regeln zur Sommer-/Winterzeit (DST = Daylight Savings Time).  Sie lauten zum Beispiel: "Im Winter hat Deutschland einen Offset von +01:00 und im Sommer von +02:00. Die Sommerzeit beginnt jeweils am letzten Sonntag im März um 02:00 Uhr CET (Central European Time), indem die Stundenzählung um eine Stunde von 02:00 Uhr auf 03:00 Uhr vorgestellt wird. Sie endet jeweils am letzten Sonntag im Oktober um 03:00 Uhr CEST (Central European Summer Time), indem die Stundenzählung um eine Stunde von 03:00 Uhr auf 02:00 Uhr zurückgestellt wird."

Hier ein Beispiel, das einige Methoden der Klasse  ZoneId zeigt:
 
 

ZoneId. getAvailableZoneIds ().stream()

      .filter(z->z.startsWith("Mexico"))

      .map(s->ZoneId. of (s))

      .map(zi->zi. toString ()+" ("

              +zi. getDisplayName (TextStyle.FULL,Locale.US)+")")

      .forEach(System.out::println);
 
 

Die Ausgabe ist:

Mexico/BajaSur (Mountain Time)

Mexico/General (Central Time)

Mexico/BajaNorte (Pacific Time)
 
 

Die Methode  getAvailableZoneIds() liefert einen  Set<String> mit den Bezeichnern aller bekannten Zeitzonen.  Wir fischen alle Strings heraus, die mit "Mexico" beginnen, erzeugen mit der  ZoneId.of() -Methode aus den Strings die entsprechenden  ZoneId -Objekte und erzeugen daraus wiederum Textdarstellungen mit den Methoden  toString() und  getDisplayNa me() .

Solche  ZoneId -Objekte werden benötigt, um  ZonedDateTime -Objekte zu erzeugen.  Hier einige Beispiele:
 
 

// July 21, 1969 at 02:56 UTC (Neil Armstrong steps onto the moon)

ZonedDateTime t = ZonedDateTime. of (1969,7,21,2,56,0,0, ZoneId.of("UTC") );
 
 

// 9. November 1989 21:15 Uhr (Mauerfall / Fall of Berlin Wall)

LocalDateTime local =  LocalDateTime.of(1989,11,9,21,15,0);

ZonedDateTime zoned =  local. atZone ( ZoneId.of("Europe/Berlin") );
 
 

Die Klasse  ZonedDateTime hat eine  of() -Methode, genau wie bei  LocalDateTime , nur mit dem Unterschied, dass man nicht nur Jahr, Monat, Tag, Stunde, Minute, Sekunde und Nanosekunde angeben muss, sondern zusätzlich noch die Zeitzone.  Natürlich kann man auch aus einer Zeitangabe ohne Zeitzone eine Zeitangabe mit Zeitzone machen (z.B. mit Hilfe der Methode  atZone() in der  LocalDateTime -Klasse).

Es gibt zahlreiche weitere Konvertierungsmöglichkeiten.  So kann man einen exakten Punkt auf der Zeitachse vom Typ  Instant in ein  ZonedDateTime -Objekt verwandeln (z.B. mit der Methode  ZonedDateTime.ofInstant ( Instant,ZoneId ) ); die umgekehrte Konvertierung von  ZonedDateTime nach  Instant lässt sich zum Beispiel mit der Methode  Instant.from( ZonedDateTime ) machen. Und vieles mehr.

Interessant ist die Konvertierung einer Zeitangabe mit Zeitzone in eine Zeitangabe in einer anderen Zeitzone.   Dafür gibt es zwei Methoden:  withZoneSameLocal(ZoneId) und  withZoneSameInstant(ZoneId) .  Hier ist ein Beispiel, das den Unterschied zwischen den beiden Methoden illustriert:
 
 

// 9. November 1989 21:15 Uhr (Mauerfall)

ZonedDateTime t = ZonedDateTime.of(LocalDate.of(1989, Month.NOVEMBER,9),

                        LocalTime.of(21,15),ZoneId.of("Europe/Berlin"));

System.out.println(t);

System.out.println(t. withZoneSameLocal (ZoneId.of("Asia/Tokyo")));

System.out.println(t. withZoneSameInstant (ZoneId.of("Asia/Tokyo")));
 
 

Heraus kommt:
 
 

1989-11-09T21:15+01:00[Europe/Berlin]
 
 

1989-11-09T21:15+09:00[Asia/Tokyo]

1989-11-10T05:15+09:00[Asia/Tokyo]
 
 

Die Methode  withZoneSameLocal(ZoneId) liefert die gleiche Uhrzeit in einer anderen Zeitzone.  Im Beispiel wird aus "21:15 in Berlin" der Zeitpunkt "21:15 in Tokio".  Das sind wegen der Zeitdifferenz zwei unterschiedliche Zeitpunkte auf der Zeitachse.

Die Methode  withZoneSame Instant (ZoneId) liefert denselben Zeitpunkt in einer anderen Zeitzone.  Im Beispiel wird aus "21:15 in Berlin" der Zeitpunkt "05:15 in Tokio".  Wegen der Zeitdifferenz war es um 21:15 Uhr in Berlin bereits 05:15 Uhr am Morgen des nächsten Tages in Tokio.

Ansonsten hat die Klasse  ZonedDateTime die gleichen Methoden wie die  LocalDateTime -Klasse:  between() liefert wieder eine Zeitspanne vom Type  Period ; es gibt  plus() minus() , und vieles mehr.  Die Methoden erschließen sich leicht mit einem Blick in die JavaDoc der  ZonedDateTime -Klasse.

Hier ein Beispiel, in dem die Ankunftszeit eines Fluges von Chicago nach Paris berechnet wird, der 8 Stunden und 10 Minuten lang dauert:
 
 

// calculate the arrival time of a flight from Chicago to Paris that takes 8 h 10 min

LocalDateTime arrival(LocalTime departure, LocalDate whichDay) {

  return ZonedDateTime. of (whichDay, departure, ZoneId.of("US/Central"))

                      . withZoneSameInstant (ZoneId.of("Europe/Paris"))

                      . plus (Duration.ofHours(8).plusMinutes(10))

                      . toLocalDateTime ();

}

Wir erzeugen den Zeitpunkt des Abflugs in Chicago mit  ZonedDateTime.of() , bestimmen mit  withZoneSameInstant() , wieviel Uhr es zum Abflugszeitpunkt am Zielort in Paris ist, addieren die Flugzeit von 8 h 10 min und konvertieren den  so berechneten Ankunftszeitpunkt  mit  toLocalDateTime() in die lokale Uhrzeit am Zielort.

Lediglich bei Kalenderberechnungen rund um den Zeitpunkt der Sommer-/Winterzeitumstellung muss man aufpassen.  Bei der Zeitumstellung entstehen Überlappungen und Sprünge.  Wie geht die  ZonedDateTime -Klasse damit um?  Hier ist ein Beispiel, in dem wir durch das Addieren von jeweils einer Stunde den Zeitpunkt der Sommer-/Winterzeitumstellung überqueren:
 
 

ZonedDateTime inGap = ZonedDateTime.of(2014,3,30,2,30,0,0,ZoneId.of("Europe/Berlin"));

for (int i = -2;i<=2;i++)

  System.out.println(Math.abs(i)+" hours "

           +((i<0)?"before":"after ")+" gap: "+inGap.plusHours(i));

}

ZonedDateTime inOverlap = ZonedDateTime.of(2014,10,26,2,30,0,0,ZoneId.of("Europe/Berlin"));

for (int i = -2;i<=2;i++)

  System.out.println(Math.abs(i)+" hours "

           +((i<0)?"before":"after ")+" overlap: "+inOverlap.plusHours(i));

}
 
 

Heraus kommt folgendes:

2 hours before gap: 2014-03-30T00:30+01:00[Europe/Berlin]

1 hours before gap: 2014-03-30T 01:30 +01:00[Europe/Berlin]

0 hours after  gap: 2014-03-30T 03:30 +02:00[Europe/Berlin]

1 hours after  gap: 2014-03-30T04:30+02:00[Europe/Berlin]

2 hours after  gap: 2014-03-30T05:30+02:00[Europe/Berlin]
 
 

2 hours before overlap: 2014-10-26T00:30+02:00[Europe/Berlin]

1 hours before overlap: 2014-10-26T01:30+02:00[Europe/Berlin]

0 hours after  overlap: 2014-10-26T 02:30 +02:00[Europe/Berlin]

1 hours after  overlap: 2014-10-26T 02:30 +01:00[Europe/Berlin]

2 hours after  overlap: 2014-10-26T03:30+01:00[Europe/Berlin]
 
 

Wie man sieht, macht die Uhrzeit den zu erwartenden Sprung vorwärts bei der Umstellung von Winter- auf Sommerzeit und die zu erwartende Überlappung bei der Umstellung von Sommer- auf Winterzeit.  Das funktioniert auch, wenn nicht nur bei Stunden, sondern auch wenn man andere Zeiteinheiten (Tage, Wochen) aufaddiert.  Nur wenn man eine Zeitspanne dazu addiert, kann es Überraschungen geben, wie das nachfolgende Beispiel demonstriert:
 
 

ZonedDateTime meetingBeforeGap = ZonedDateTime.of(2014,3,27,9,00,0,0,ZoneId.of("Europe/Berlin"));

ZonedDateTime meetingOneWeekLater;

meetingOneWeekLater = meetingBeforeGap.plus(Duration.ofDays(7));   //  incorrect !!!

meetingOneWeekLater = meetingBeforeGap.plus(Period.ofDays(7));
 
 

Heraus kommt folgendes:

meeting before DST gap            : 2014-03-27T09:00+01:00[Europe/Berlin]

meeting a week later after DST gap: 2014-04-03T 10:00 +02:00[Europe/Berlin]

meeting a week later after DST gap: 2014-04-03T 09:00 +02:00[Europe/Berlin]
 
 

Wenn wir die Zeitspanne von einer Woche als  Duration ausdrücken, dann werden tatsächlich 7 Tage hinzuaddiert - ohne Berücksichtigung der Zeitumstellung.  Der neu berechnete Termin liegt dann genau 7 x 24 Stunden später.

Wenn wir die Zeitspanne von einer Woche als  Period ausdrücken, dann wird die Zeitumstellung einkalkuliert.  Der neu berechnete Termin ist 7 Tage später um die gleiche Uhrzeit.

Generell berücksichtigen die Abstraktionen aus dem Bereich der "menschlichen Zeit" die Zeitzonen-Regeln, während die Abstraktionen aus dem Bereich der "kontinuierlichen Zeit" nur mit absoluten Zeitspannen und Zeitpunkten rechnen.

Formatieren und Parsen

Ein wichtiger Aspekt ist die Konvertierung von Strings in Date/Time-Objekte und vice versa.  Für das Formatieren von Date/Time-Objekten stehen  format() -Methoden in der Klasse  DateTimeFormatter im Package  java.time.format zur Verfügungen.  Für das Parsen gibt es statische  parse() -Methoden in den verschiedenen Klassen des Date & Time APIs. 

Schauen wir uns zunächst die Klasse DateTimeFormatter näher an.  Sie unterstützt drei Arten von Formatierern:

- vordefinierte Standard-Formate,

- lokalisierte Formate und

- selbstdefinierte Formate.

Die Liste der vordefinierten Formate ist lang, wie ein Blick in die JavaDoc zeigt.  Die meisten Formate sind standardisierte Formate, in erster Linie für technische Zwecke, d.h. bisweilen wenig benutzerfreundlich.  Einige Beispiele:
 
 

ZonedDateTime fallOfBerlinWall

= LocalDateTime.of(1989,11,9,21,15).atZone(ZoneId.of("Europe/Berlin"));

String s = DateTimeFormatter. ISO_DATE_TIME .format(fallOfBerlinWall);

       s = DateTimeFormatter. ISO_WEEK_DATE .format(fallOfBerlinWall);

       s = DateTimeFormatter. RFC_1123_DATE_TIME .format(fallOfBerlinWall); 
 
 

Heraus kommt:

1989-11-09T21:15:00+01:00[Europe/Berlin]

1989-W45-4+01:00

Thu, 9 Nov 1989 21:15:00 +0100
 
 

Benutzerfreundlicher und für menschliche Wesen leichter lesbar sind die lokalisierten Formate.  Es gibt sie in 4 Ausprägungen: SHORT, MEDIUM, LONG und FULL.  Die verwendete Locale ist entweder die Default-Locale auf dem jeweiligen Rechner oder eine explizit mit  withLocale() spezifizierte Locale.  Hier ein Beispiel:
 
 

for (FormatStyle fs : FormatStyle.values()) {

  DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(fs);

  System.out.println(formatter.format(fallOfBerlinWall));

}
 
 

Wir formatieren ein  ZonedDateTime -Objekt in allen 4 Ausprägungen.  Heraus kommt auf einem Rechner mit deutscher Default-Locale:

Donnerstag, 9. November 1989 21:15 Uhr MEZ

9. November 1989 21:15:00 MEZ

09.11.1989 21:15:00

09.11.89 21:15
 
 

Mit einer italienischen Locale sieht es so aus:
 
 

formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)

                             . withLocale (Locale.ITALY);

System.out.println(formatter.format(fallOfBerlinWall));
 
 

Heraus kommt:

giovedì 9 novembre 1989 21.15.00 CET
 
 

Benutzerdefinierte Formate werden über Patterns beschrieben.  Ein Pattern besteht aus mehreren Buchstaben, von denen jeder für einen bestimmten Teil des Datums steht.  Die Häufigkeit der Wiederholung des Buchstaben bestimmt dann letztlich das Format für den betreffenden Teil des Datums.  Die JavaDoc der Klasse  DateTimeFormatter enthält eine ausführliche Beschreibung der Patterns.  Hier einige Beispiele zur Illustration:
 
 

DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm").format(dt);

DateTimeFormatter.ofPattern("MMMM dd, yyyy HH:mm xx").format(dt);

DateTimeFormatter.ofPattern("EEEE dd.MM.yyyy KK:mm a VV").format(dt);

DateTimeFormatter.ofPattern("d.M.yy H:mm O").format(dt);
 
 

Heraus kommt:

Do 1989-11-09 21:15

November 09, 1989 21:15 +0100

Donnerstag 09.11.1989 09:15 PM Europe/Berlin

9.11.89 21:15 GMT+1
 
 

Das Parsen geht analog.  Es erfolgt per Default mit dem Standard-Format ISO_LOCAL_DATE.  Wenn man ein anderes Format parsen will, muss man der  parse() -Methode ein  DateTimeFormatter -Objekt mitgeben.  Hier zwei Beispiele:
 
 

LocalDate birthdayMahatmaGandhi = LocalDate.parse("1869-10-02");

ZonedDateTime fallOfBerlinWall = ZonedDateTime.parse("1989-11-09 21:15 +0100",

                                                      DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm xx"));
 
 

Im ersten Beispiel wird mit dem Standard-Format ISO_LOCAL_DATE geparst.  Im  zweiten Beispiel ist es ein benutzerdefiniertes Format.

Interoperabilität

Schließlich stellt sich noch die Frage, wie sich die neuen Date & Time Abstraktionen zu den alten Klassen sie  Date Calendar , etc. verhalten.  Hier ist es so, dass zahlreiche Konvertierungsmöglichkeiten geschaffen wurden, damit die alten Abstraktionen in die neuen verwandelt werden können und umgekehrt.  Die nachfolgende Tabelle gibt eine Übersicht über die Konvertierungen.
 
 
 
Beteiligte Typen
&larr;
&rarr;
Instant &larr;&rarr; java.util.Date
date.toInstant()
Date.from(instant)
ZonedDateTime &larr;&rarr; GregorianCalendar
cal.toZonedDateTime()
GregorianCalendar.from(zonedDateTime)
Instant &larr;&rarr; java.sql.Timestamp
timestamp.toInstant()
TimeStamp.from(instant)
LocalDateTime &larr;&rarr; java.sql.Timestamp
timeStamp.toLocalDateTime()
Timestamp.valueOf(localDateTime)
LocalDate &larr;&rarr; java.sql.Date
date.toLocalDate()
Date.valueOf(localDate)
LocalTime &larr;&rarr; java.sql.Time
time.toLocalTime()
Time.valueOf(localTime)
DateTimeFormatter &rarr; java.text.DateFormat
formatter.toFormat()
---
ZoneId &larr;&rarr; java.util.TimeZone 
timeZone.toZoneId()
Timezone.getTimeZone(id)
Instant &larr;&rarr; java.nio.file.attribute.FileTime 
fileTime.toInstant()
FileTime.from(instant)

Zusammenfassung

Der JDK 8 hat ein neues Date & Time API, das ältere Date/Time-Abstraktionen wie  Date Calendar DateFormat , etc. ablöst. Im neue Date & Time API sind alle Abstraktionen unveränderlich und damit threadsicher.  Das API unterstützt den Fluent Programming Style, d.h. die Verkettung von Operationen. Die wesentlichen neuen Abstraktionen sind:

- Instant , ein Punkt auf der kontinuierlichen Zeitachse in Nanosekunden-Genauigkeit

- Duration , die Differenz zwischen zwei Zeitpunkten auf der kontinuierlichen Zeitachse

- LocalDateTime LocalDate LocalTime , Zeitangaben ohne Zeitzone ausgedrückt in Feldern wie Jahr, Monat, Tag, Stunde, Minute, Sekunde, Nanosekunde.

- MonthDay YearMonth Year , unvollständige Zeitangaben

- ZonedDateTime , Zeitangaben mit Zeitzone ausgedrückt in Feldern

- ZoneId ZoneOffset ZoneRules , zur Beschreibung von Zeitzonen

- Period , die Differenz zwischen zwei Zeitpunkten ausgedrückt in Feldern

- Month DayOfWeek ChronoUnit , diverse Enum-Typen für Monate, Wochentage, Zeiteinheiten

- DateTimeFormatter , zum Formatieren und Parsen von Date/Time-Angaben

Der Überblick über das neue Date & Time API, den wir in diesem Beitrag gegeben haben, ist naturgemäß unvollständig.  Weder haben wir sämtliche Operationen vorgestellt noch haben wir die Erweiterung um eigene Kalenderabstraktionen betrachtet.  Vielmehr ging es darum, einen ersten Eindruck zu vermitteln.  Das intelligente Design und die umfangreiche JavaDoc des neuen Date & Time API machen es relativ leicht, sich den Rest bei Bedarf selbst zu erarbeiten.
 
 

Literaturverweise

/COLE/ 
Video von Stephen Colebourne's Vortrag über das Date/Time API, JAX 2013


URL: http://jaxenter.de/videos/JAX-TV-The-new-JDK-18-Date-and-Time-API-171396

/TUT/      
Data-Time Tutorial by Oracle


URL: http://docs.oracle.com/javase/tutorial/datetime/TOC.html

/JVONE1/  
Roger Riggs & Stephen Colebourne: Introducing the Java Time API in JDK 8, JavaOne 2013


URL: https://oracleus.activeevents.com/2013/connect/sessionDetail.ww?SESSION_ID=6064

/JVONE2/
Xueming Shen, Roger Riggs & Stephen Colebourne: Converting to the New Date and Time API in JDK 8, JavaOne 2013


URL: https://oracleus.activeevents.com/2013/connect/sessionDetail.ww?SESSION_ID=6091

/JSR310/   
JSR 310: Date & Time API


URL: https://jcp.org/aboutJava/communityprocess/final/jsr310/index.html

/JEP150/
JEP 150: Date & Time API


URL: http://openjdk.java.net/jeps/150

/JODA/  
JodaTime-Bibliothek


URL: http://www.joda.org/joda-time/

Die gesamte  Serie über Java 8:

/JAV8-0/ Neue Features in Java 8 - Überblick
Klaus Kreft & Angelika Langer, Java Magazin, März 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/73.Java8.Overview/73.Java8.Overview.html
/JAV8-1/ Funktionale Programmierung in Java
Klaus Kreft & Angelika Langer, Java Magazin, September 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/70.Java8.FunctionalProg/70.Java8.FunctionalProg.html
/JAV8-2/ Lambda-Ausdrücke und Methoden-Referenzen
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/71.Java8.Lambdas/71.Java8.Lambdas.html
/JAV8-3/ Default-Methoden und statische Methoden in Interfaces
Klaus Kreft & Angelika Langer, Java Magazin, Februar 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/72.Java8.DefaultMethods/72.Java8.DefaultMethods.html
/JAV8-4/ Übersicht über das Stream API in Java 8
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/74.Java8.Streams-Overview/74.Java8.Streams-Overview.html
/JAV8-5/ Stream-Erzeugung und Stream-Operationen
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/75.Java8.Fundamental-Stream-Operations/75.Java8.Fundamental-Stream-Operations.html
/JAV8-6/ Stream-Kollektoren und die Stream-Operation collect()
Klaus Kreft & Angelika Langer, Java Magazin, September 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/76.Java8.Stream-Collectors/76.Java8.Stream-Collectors.html
/JAV8-7/ Stateful Lambdas - Regeln für die Seiteneffekte in Lambda-Ausdrücken, die an Stream-Operationen übergeben werden
Klaus Kreft & Angelika Langer, Java Magazin, November 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/77.Java8.Streams-and-Statefulness/77.Java8.Streams-and-Statefulness.html
/JAV8-8/ Das Date/Time API
Klaus Kreft & Angelika Langer, Java Magazin, Januar 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/78.Java8.Date-Time-API/78.Java8.Date-Time-API.html
/JAV8-9/ CompletableFuture
Klaus Kreft & Angelika Langer, Java Magazin, März 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/79.Java8.CompletableFuture/79.Java8.CompletableFuture.html
/JAV8-10/ Optional<T>
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/80.Java8.Optional-Result/80.Java8.Optional-Result.html

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
Lambdas & Streams - Java 8 Language Features and Stream API & Internals
3 day seminar ( open enrollment and on-site)
Java 8 - Lambdas & Stream, New Concurrency Utilities, Date/Time API
4 day seminar ( open enrollment and on-site)
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
Related Reading
Lambda & Streams Tutorial & Reference
In-Depth Coverage of all aspects of lambdas & streams
Lambdas in Java 8
Conference Presentation at JFokus 2012 (slides)
Lambdas in Java 8
Conference Presentation at JavaZone 2012 (video)
 

 
  © Copyright 1995-2016 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/78.Java8.Date-Time-API/78.Java8.Date-Time-API.html  last update: 2 Aug 2016