Neo Tech Blog

NEO TECH LBLOG - Praxis-Techblog für IT, Web und Mobile
Martin Gerlach

Java SE 8 Neuerungen (Teil 3): Date/Time-API und warum ein Umstieg auf Java 8 lohnt

1 Kommentar


Im ersten Teil dieser Mini-Serie über Neuheiten in Java SE 8 wurden Lambda-Ausdrücke sowie Default-Methoden in Interfaces vorgestellt. Im zweiten Teil ging es dann um die Streaming API, in der diese neuen Features vielfach zum Einsatz kommen.
Der dritte und letzte Teil gibt einen Überblick über verschiedene weitere Neuerungen in Java 8 und nennt einige Vorteile eines schnellen Umstiegs auf die neue Plattform.

Die neue Date/Time-API in Java 8

Entwicklern, die zuvor bereits mit der Bibliothek Joda-Time gearbeitet haben, werden die neuen APIs im Package java.time und dessen Subpackages bekannt vorkommen. In der Tat ist der Hauptentwickler von Joda-Time, Stephen Colebourne, einer der Specification Leads des JSR-310.

Wie Oracle selbst beschreibt, bestehen die wesentlichen Schwachpunkte der bisherigen API, hauptsächlich Date, Calendar und SimpleDateFormat, insbesondere darin, dass Objekte dieser Klassen nicht threadsafe sind (Objekte von Date und Calendar sind mutable aber nicht synchronisiert und SimpleDateFormat ist intern unnötig zustandsbehaftet), sowie in einigen nicht-intuitiven Design-Entscheidungen, die noch auf frühe Java-Tage zurück gehen. Zum Beispiel repräsentiert der Wert 0 für das Jahresfeld in der Date-Klasse das Jahr 1900 und die Monate sind nicht von 1 bis 12 nummeriert, sondern von 0 bis 11. Zwar wurde mit der Calendar-Klasse in Java 1.1 eine etwas flexiblere, für Internationalisierung geeignetere Lösung (inkl. Zeitzonen und nicht-gregorianischen Kalendern) eingeführt und die Date-Klasse im Prinzip zum Wrapper um die Unix-Epoche (Anzahl der Millisekunden seit dem 1.1.1970 0:00 UTC) degradiert, aber auch Calendar ist nicht threadsafe und mit unter nicht sehr intuitiv zu benutzen.

Die Klassen des Packages java.time folgen dagegen den Prinzipien Immutability (“Value Classes”) und Domain Driven Design, insb. der Trennung von ortsabhängigen Datumswerten und ortsunabhängigen Zeitbegriffen, also genau von “Date” und “Time”.

Weiterhin wurde auf eine lesbare, intuitiv verständliche API geachtet, wie folgendes Beispiel von Datumsarithmetik über den Beginn der mitteleuropäischen Sommerzeit hinweg zeigt:

LocalDateTime savingTest =

LocalDateTime.of(2014, Month.MARCH, 28, 23, 30);

savingTest.plusHours(48); // „2014-03-30T23:30“

savingTest.plusDays(2);   // „2014-03-30T23:30“

 

ZonedDateTime atZone = savingTest.atZone(ZoneId.of(„Europe/Berlin“));

atZone.plusHours(48);    // „2014-03-31T00:30+02:00[Europe/Berlin]“

atZone.plusDays(2);     // „2014-03-30T23:30+02:00[Europe/Berlin]“

Abgesehen davon, dass die “plus”-Operationen hier jeweils neue Objekte erzeugen (im Kommentar jeweils die durch .toString() generierte String-Repräsentation der neuen Objekte), ist deren Semantik auch sehr verständlich:

Liegt keine Information zur Zeitzone vor (LocalDateTime), so entsprechen 48 Stunden immer zwei Tagen.

In Zeitzonen mit Sommerzeit (ZonedDateTime) ist dies um die Umstellungszeitpunkte herum aber nicht immer der Fall. Hier geht man von der intuitiven Interpretation der addierten Zeiträume aus:

  • Für “48 Stunden” zählt man die Uhr 48 Stunden vorwärts und dabei wird zur Umstellung Ende März für die MESZ dann eine Stunde übersprungen, so dass man bis zum 31.3. 0:30 zählt.
  • Addiert man zu einem beliebigen Zeitpunkt jedoch “2 Tage”, so geht man von der selben Uhrzeit “übermorgen” aus, also 30.3. 23:30. Dies sind im angegebenen Beispiel jedoch nur 47 Stunden vom Ausganszeitpunkt (“atZone”) gesehen.

Analog:

LOG.info(atZone);        // „2014-03-28T23:30+01:00[Europe/Berlin]“

 

Period twoDays = Period.ofDays(2);                    // „P2D“

Period.between(                                    // „P2D“

atZone.toLocalDate(), atZone.plusDays(2).toLocalDate());

 

Duration fortyEightHours = Duration.ofDays(2);            // „PT48H“

Duration.between(atZone, atZone.plusDays(2));             // „PT47H“

 

Period (“P”, Datumsunterschied in Tagen) und Duration (“Time Period” bzw. “PT”, Zeitunterschied mit Nanosekunden-Präzision) sind dabei die beiden zentralen Klassen für Zeiträume, mit gemeinsamem Interface TemporalAmount.

Zusätzlich werden auch andere “Chronologies” als das ISO-8601-System wie “Japanese” oder “ThaiBuddhist” unterstützt. Die entsprechenden Klassen befinden sich im Package java.util.chrono.
Den alten APIs wurden Konvertierungsfunktionen hinzugefügt, z.B.:

static GregorianCalendar GregorianCalendar.from(ZonedDateTime)

static Date Date.from(Instant)

ZonedDateTime GregorianCalendar.toZonedDateTime()

Instant Calendar.toInstant()

Instant Date.toInstant()

Instant-Objekte repräsentieren dabei – ähnlich wie Date, nur eben immutable – Zeitpunkte der Unix-Epoche mit Nanosekunden-Präzision. Intern werden diese Zeitpunkte durch je einen long-Wert für die Sekunden der Epoche sowie einen int-Wert für den Nanosekundenbruchteil (0 … 999.999.999) repräsentiert.

Weitere Java 8 Neuerungen

Alle Neuerungen des JDK 8 sind unter What’s New in JDK 8 nachzulesen, hier eine kleine Auswahl:

Hinsichtlich Concurrency wurde neben der Unterstützung paralleler Streams (siehe Teil 2), das Collections-Framework massiv um Interfaces, Klassen und Methoden erweitert. Siehe Package java.util.concurrent (mit Subpackages) sowie z.B. ConcurrentHashMap als Beispiel für eine so erweiterte Klasse (über 30 neue Methoden). Außerdem gibt es mit java.util.concurrent.atomic.LongAccumulator, LongAdder, DoubleAccumulator, sowie DoubleAdder durchsatzoptimierte neue Werkzeuge zum parallelen aktualisieren (speziell addieren bzw. aggregieren) von Zahlenwerten durch zahlreiche Threads.

In der HotSpot JVM wurde der PermGen Space durch den sog. “Metaspace” (native memory) ersetzt, das Compilerflag -XX:PermGenSpace ist damit unnötig und wirkungslos.

Es ist nun möglich vom Compiler Informationen zur Reflection für Parameternamen (javac -parameters) im Bytecode hinterlegen zu lassen. Einige Frameworks, wie z.B. Spring, werden davon profitieren, denn bisher war hierfür das Kompilieren im Debug-Modus nötig.

Repeatable Annotations können nun ohne ihre Container-Annotation geschrieben werden (z.B. mehrere @Schedule Annotationen in Spring Batch oder mehrere @CompoundIndex Annotationen in Spring Data), allerdings handelt es sich hier nur um vereinfachte Syntax und keine Änderung im Bytecode, d.h. die Reflection erfolgt nach wie vor über die weiterhin benötigte Container-Annotation.

Einige Funktionen für vorzeichenlose Ganzzahlarithmetik und Parsing für Long und Integer wurden hinzugefügt. Hierbei handelt es sich nicht um neue Typen! Zum Beispiel:

// This raised and still raises a NumberFormatException

long s = Long.parseLong(„ffffffffffffffff“, 16); // Exception !!!

 

// New in Java 8

long u = Long.parseUnsignedLong(„ffffffffffffffff“, 16); // Now OK!

LOG.info(Long.toString(u)); // „-1“ … oops?! See below!

LOG.info(Long.toUnsignedString(u, 16)); // „ffffffffffffffff“ … ah!

 

Hier ist zu beachten, dass die Variable u intern tatsächlich den Wert -1 hat. Dies ist die vorzeichenbehaftete Interpretion des Bitmusters aus 64 Einsen bzw. 16 mal “f” im Hexadezimalsystem im Wertebereich [ -263, 263 – 1 ] (64 Bit), also genau im Wertebereich von Java long Werten. Im Wertebereich [ 0, 264 – 1 ] (vorzeichenlose 64 Bit), würde das Bitmuster tatsächlich als 264 – 1 interpretiert werden.

Im JDK8 gibt es nun (endlich!) einen Base64 Encoder und Decoder. JavaFX und die Security-Packages wurden erweitert und an diversen Stellen wurden Performance-Verbesserungen erzielt.
Es gibt außerdem 3 “Compact Profiles” (Java-Untermengen) für die Unterstützung entsprechender Devices und Appliances, sowie eine auf der JVM basierende und dadurch mit Java interaktionsfähige Javascript Engine namens Nashorn (“jjs”).

Fazit

Die Version 8 der Sprache Java und des JDK bringt umfassende Neuerungen. Zentrale Punkte sind die Unterstützung von funktionalen Elementen wie Lambda-Ausdrücken und Streams. Um die bestehenden APIs, insb. das Collection-Framework, hiermit “aufzurüsten”, wurden Default- und Static-Methoden auf Interfaces eingeführt. Die neue Date/Time-API, inspiriert von der Bibliothek Joda-Time, folgt ebenfalls wichtigen in der funktionalen Programmierung üblichen Design-Prinzipien wie z.B. Immutability (Value Classes). Multicore-Computing wird durch parallele Streams und Concurrency-Erweiterungen unterstützt.

Alles in allem werden dem Entwickler damit mächtige und einigermaßen moderne Werkzeuge in die Hand gegeben.

Schwachpunkte beJava 8

Trotzdem gibt es – gerade hinsichtlich Lambdas und Streams – noch einige Schwachstellen:

Zum einen führt Javas Typsicherheit dazu, dass die Zuweisung eines Lambda-Ausdrucks an Variablen und Parameter nur über funktionale Interfaces möglich ist, die u.U. inkompatibel zu anderen funktionalen Interfaces sind, für die der Lambda-Ausdruck aber ebenso eingesetzt werden kann!

Zum anderen muss Code, welcher so typisierte Lambdas (etwa als Function<T, R> oder IntFunction<T>) entgegen nimmt, beim Aufruf der Funktion den Methodennamen des funktionalen Interfaces benutzen, da die Java-Syntax (noch) keine reinen Funktionsaufrufe kennt. Z.B.:

IntFunction<String> f = String::length; // or: f = s -> s.length();

int len = f(myString); // That would be nice but does not compile

int len = f.apply(myString); // Need to name the method :/

Es kann nicht f(myString) aufgerufen werden, sondern es muss f.apply(myString) verwendet werden, da die abstrakte Methode von IntFunction<T> eben int apply(T) ist.

Zu Guter Letzt sollte man einmal darauf achten, wie viele (nämlich die meisten) der neuen Interfaces, Klassen und Methoden zur Verwendung von Lambdas und Streams nur der Unterstützung der primitiven Java-Typen int, long und double dient (boolean, byte, short und float wurden berechtigterweise gleich außen vor gelassen). Siehe auch das vorige Beispiel, hier wurde IntFunction<String> statt Function<String, Integer> verwendet, da Java Generic Type Parameter keine primitiven Typen unterstützen und man hier aber eben auf jeden Fall int (und nicht Integer) erhalten möchte.

Performance ist hierbei mittlerweile kaum noch ein Argument, da Autoboxing-/-unboxing auf modernen Rechnerarchitekturen kaum noch einen Unterschied zur reinen Verwendung primitiver Typen machen dürfte.

Nur über “null”-Werte müsste man sich noch etwas Gedanken machen, dann stünde einer Abschaffung von primitiven Typen nicht mehr viel im Wege (s. z.B. folgenden etwas älteren Artikel: “For the Java Development Kit (JDK) 10 or after, a fundamental change is being discussed: making the Java language Object Oriented. This might see the introduction of a unified type system that turns everything into objects and means no more primitives.”)

Vorteile von Java 8 für Unternehmen

Die o.g. Neuerungen und die sich daraus ergebenden Perspektiven überwiegen die genannten Schwachstellen bei Weitem.

Für Unternehmen mit eigener Softwareentwicklung lohnt sich ein schrittweiser Umstieg auf Java 8 aus mehreren Gründen:

Die neuen Sprach-Features schließen eine Lücke zu den sich insbesondere in der Verarbeitung großer Datenmengen und der Datenanalyse (“Big Data”) immer weiter verbreitenden JVM-Sprachen mit funktionalen Features, die nicht an einen abwärtskompatiblen Standard gebunden sind (Clojure, Scala, usw.) sowie zu JavaScript, welches sich auch serverseitig immer stärkerer Beliebtheit erfreut.
Für erfahrene Java-Entwickler und Anwendungs-Architekten sind die Neuerungen leicht zu erlernen und mit etwas Grundwissen über funktionale Prinzipien ebenso leicht im Alltag anzuwenden. Dabei geht es nicht nur um lokale Optimierungen von Code-Stellen zwecks Lesbarkeit, sondern, mit zunehmender Routine, nach und nach auch um die Weiterentwicklung ganzer Anwendungsarchitekturen im Hinblick auf Multicore- und Cluster-Computing, Performance, Skalierbarkeit und Elastizität. Funktionale Prinzipien wie Immutability, die daraus unmittelbar folgende Threadsafety, sowie Streaming usw., erleichtern zusammen mit Techniken der reaktiven oder ereignisorientierten Programmierung sowie neuartigen Virtualisierungslösungen (Infrastructure as Code, as a Service, …) die Erstellung effizienter, skalierbarer, und wartbarer Systeme.

Überblick über alle Neuerungen im JDK 8: What’s New in JDK 8.

 

Zurück zu Teil 1: Java 8 Neuerungen – Lambda Ausdrücke und Default Methoden

Zurück zu Teil 2: Java 8 Neuerungen – Stream API

___

Wir suchen Java Developer:

Software Developer Java (m/w)

Sr. Java Developer F&E – Schwerpunkt Computerlinguistik

Sr. Java Developer (m/w) F&E – Schwerpunkt Machine Learning/ Big Data

Senior Java Developer (m/w) – Schwerpunkt Solr / Lucene / Elastic Search

Bewirb Dich jetzt!

Martin Gerlach

Autor: Martin Gerlach

Martin Gerlach arbeitet seit 2013 als Senior Softwareentwickler im Bereich Angebotsimport bei idealo, Deutschlands größtem Preisvergleichsportal. Dort befasst er sich mit der Entwicklung perfomanter, skalierbarer Datenimport-, Transformations- und Analysetools. Sein besonderes Interesse gilt dabei verteilten Frameworks für Streaming und Analyse großer Datenmengen sowie damit einhergehenden "funktionalen" Ansätzen in der Programmierung. Vorher war Martin über 8 Jahre lang für IBM und 5 Jahre für Neofonie sowohl in Forschung und Entwicklung als auch in Kundenprojekten tätig. Er ist Master of Science Absolvent der HAW Hamburg in Informatik mit Schwerpunkt "Verteilte Systeme".

Ein Kommentar

  1. Vielen Dank für den interessanten Beitrag 🙂

Schreibe einen Kommentar

Pflichtfelder sind mit * markiert.