|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Effective Java
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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. MitDate 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 APIEs 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 ZeitDie 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 ZeitzoneEin 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 - ZeitspanneDie 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 DatumsberechnungenFü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 ZeitzoneZeitangaben 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 ParsenEin 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ätSchließ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.
ZusammenfassungDer 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
Die gesamte Serie über Java 8:
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
© Copyright 1995-2018 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: 26 Oct 2018 |