Stream-API in Java SE 8

Stream-API in Java SE 8

Im ersten Teil dieser Serie ĂĽber Neuheiten in Java SE 8 wurden Lambda AusdrĂĽcke sowie Default-Methoden in Interfaces vorgestellt. Im zweiten Teil wird der ehemalige Neofoniker und jetzige Senior Softwareentwickler bei idealo, Martin Gerlach, eine weitere Neuerung in Java 8 vorstellen: die Stream API. Mit ihr sollen Zugriffe auf Daten vereinfacht werden.

Mit Hilfe der Lambda Ausdrücken und den Default-Methoden ist es leicht möglich Operationen auf (potentiell unendlich langen) Datenströmen (Streams) im funktionalen Stil auszuführen. Ein Beispiel dafür sind z.B. Map/Reduce-Operationen auf Collections. Auch andere, in der Analyse großer Datenmengen übliche Funktionen, lassen sich damit durchführen. Zudem besteht die Möglichkeit, bestimmte Operationen auf einfache Weise automatisch parallel durch mehrere Threads auszuführen zu lassen.

Streams im Ăśberblick und erste Beispiele

Die Package-Dokumentation von java.util.stream gibt einen ausfĂĽhrlichen Ăśberblick ĂĽber die Stream-API. DarĂĽber hinaus wurden mittles Default-Methoden diverse Interfaces des Collection-Frameworks sowie vereinzelte weitere Interfaces und Klassen um Streaming-Features erweitert, ein Beispiel ist die Methode java.io.BufferedReader.lines(). Einen Ăśberblick ĂĽber die Erweiterungen des JDK hinsichtlich Streams und Lambdas findet man hier. Eine vollständige Behandlung des Themas wĂĽrde hier den Rahmen sprengen. Dieser Artikel gibt daher lediglich einige Beispiele.

Ausgangspunkt sind die neuen Default-Methoden java.util.Collection.stream() sowie die Variante fĂĽr parallele Verarbeitung, .parallelStream(), mittels der man von jeder Implementierung von Collection<E> einen Stream<E> ĂĽber die Elemente der Collection erhält. Durch Aufruf dieser Methoden findet zunächst noch kein Verarbeitungsschritt statt, sondern es werden mittels verschiedener “Fluent API Calls” die durchzufĂĽhrenden Operationen als Sequenz von intermediären (“intermediate”) Operationen und maximal einer abschlieĂźenden (“terminal”) Operation definiert. z.B.:

List<Point> points;                 // java.awt.Point(int x, int y)

// Map-Reduce

int sumYForPositiveX = points.stream()

.filter(p -> p.x > 0)                    // intermediate

.mapToInt(p -> p.y)                    // intermediate

.reduce(0, Integer::sum); // = .sum()        // terminal

// GroupBy-(Map)-Reduce

Map<Integer, Integer> sumYbySignumX = points.stream()

.collect(Collectors.groupingBy(            // terminal

p -> Integer.signum(p.x),                    

Collectors.summingInt(p -> p.y)));

// Map-Collect

List<Double> distancesFromOrigin = points.stream()  

        .mapToDouble(p -> p.distance(0.0, 0.0))            // i.

        .collect(Collectors.toCollection(ArrayList::new));    // t.

International Iteration bei Java 8

Im Unterschied zum Interface Iterator<E>, mittels dem der Programmierer die Iteration ĂĽber Iterable<E>-Implementierungen (z.B. auch Implementierungen von Collection<E>) von auĂźen (“External Iteration”) steuern kann (bzw. muss!), liegt der Fokus von Stream<E> auf der DurchfĂĽhrung bestimmter Operationen auf der zugrundeliegenden Datenstruktur. Implementierungen von Stream<E> sowie der Varianten fĂĽr primitive Typen (IntStreamLongStreamDoubleStream und diverse auf primitive Typen zugeschnittene Methoden) stellen fĂĽr die zahlreichen Methoden dieses Interfaces Implementierungen bereit, die sich intern um die AusfĂĽhrung dieser Operationen kĂĽmmern und u.a. auch das Iterieren ĂĽber die Elemente der zu analysierenden Datenstruktur ĂĽbernehmen. Das nennt man “Internal Iteration”. So wird es dem Entwickler ermöglicht, durch Angabe der verschiedenen Operationen mittels der Streaming-API auszudrĂĽcken, was mit seiner Datenstruktur gemacht werden soll und es der im JDK enthaltenen Implementierung der API zu ĂĽberlassen, wie dies geschieht.

BrĂĽckenfunktionalitäten zwischen den beiden Welten bieten BaseStream.iterator() sowie Iterator().forEachRemaining() und Iterable.forEach(). Einen Iterator als Stream-Quelle zu benutzen ist etwas komplizierter (siehe Stackoverflow zum erstenzweiten und dritten).

Eigenschaften von Streams

Streams sind:

Persistenzlos â€“ Streams speichern keine Daten, sondern dienen dazu, Daten von einer Quelle (Datenstruktur, I/O-Ressource, …) durch eine Abfolge (“Pipeline”) von Operationen zu verarbeiten und abschlieĂźend zu aggregieren, einer Datensenke zuzufĂĽhren, o.ä.

Funktional â€“ Streams verändern ihre Datenquelle nicht, Stream-Operationen erzeugen vielmehr neue Streams mit veränderten Eigenschaften. filter() löscht z.B. keine Elemente aus einer zugrundeliegenden Collection, sondern erzeugt einen neuen Stream, der bestimmte Elemente der Collection auslässt.

Lazy â€“ Intermediäre Operationen sind immer lazy, d.h. sie werden nicht bei ihrem Aufruf, sondern erst bei Aufruf einer terminalen Operation ausgefĂĽhrt. Dies hat den Vorteil, dass Abfolgen von Operationen, wie z.B. im Falle von “Finde den ersten String mit drei aufeinanderfolgenden Vokalen” (.stream().filter(s -> contains3ConsecutiveVowels(s)).findFirst()), optimiert werden können. In diesem Beispiel muss eben nicht (immer) jeder String untersucht werden.

Möglicherweise unendlich â€“ Streams ĂĽber Collections sind endlich, aber Streams mĂĽssen nicht endlich sein. Durch “abkĂĽrzende” (short-circuiting) Operationen (s.u.) wie limit()oder findFirst() sind endliche Berechnungen ĂĽber unendliche Streams möglich. Es lässt sich auch eine langlaufende terminale Operation vorstellen, die durch zustandslose intermediäre Operationen wie etwa map() und filter() verarbeitete Elemente eines unendlich langen (Input)Streams fortlaufend per Seiteneffekt (s.u.) in eine Datensenke schreibt.

Consumable â€“ Wie im Falle eines Iterators können die Elemente eines Streams nur einmal durchlaufen werden d.h., es kann nur max. eine terminale Operation ausgefĂĽhrt werden, die dazu fĂĽhrt, dass vorgelagerte intermediäre Operationen (in optimierter Art und Weise) ablaufen. Streams können also vor Aufruf der ersten terminalen Operation nicht zwischengespeichert werden, um weitere terminale Operationen auszufĂĽhren:

Stream<Integer> xStream  = points.stream().map(p -> p.x);    // ok    

List<Integer> x1 = xStream.collect(Collectors.toList());    // ok

List<Integer> x2 = xStream.collect(Collectors.toList());    // Boom!

// IllegalStateException – … stream has already been operated upon …

Erlaubt ist hingegen das Speichern der Streamdefinition als Lambda, welches dann mehrfach ausgefĂĽhrt wird:

Supplier<Stream<Integer>> s = () -> points.stream().map(p -> p.x);   

List<Integer> x1 = s.get().collect(Collectors.toList());    // ok

List<Integer> x2 = s.get().collect(Collectors.toList());    // ok!

Sequentiell oder Parallel â€“ Streams können “parallel sein”, was dazu fĂĽhrt, dass viele (aber nicht alle) Operationen parallel ablaufen. Zum Einsatz kommt dabei der common ForkJoinPool der JVM. AuĂźer ĂĽber die in der JavaDoc beschriebenen System-Properties kann man leider keinen Einfluss auf den verwendeten Pool nehmen. Weiterhin muss man damit rechnen, dass bestimmte Operationen die AusfĂĽhrung durch Synchronisationsmechanismen sogar verlangsamen, so wird z.B. sorted() immer nur single-threaded ausgefĂĽhrt und vorher wie hinterher mittels Barriers synchronisiert. Ist das Sortieren die einzige oder die mit Abstand umfassendste Aufgabe des Streams vor der terminalen Operation, so macht die Verwendung eines parallelen Streams hier keinen Sinn.

Java-8-Stream-APIs erzeugen mit wenigen Ausnahmen wie Collection.parallelStream() per default sequentielle Streams, welche mittels BaseStream.parallel() in parallele umgewandelt werden können. BaseStream.sequential() erklärt umgekehrt einen Stream fĂĽr sequentiell.

Geordnet oder Ungeordnet â€“ Streams ĂĽber geordnete Collections wie etwa Listen oder sortierte Sets sind geordnet, Streams ĂĽber HashSets sind es z.B. nicht. Operationen wie sorted() wandeln einen nicht geordneten Stream in einen geordneten um, und die Operation unordered() ermöglichst es den Stream fĂĽr nachfolgende Operationen als nicht geordnet zu betrachten, was die parallele AusfĂĽhrung von Operationen wie distinct() oder gruppierte Reduktion (grouped reduction, “group by”) i.d.R. etwas effizienter macht.

FĂĽr identisch geordnete Streams muss die AusfĂĽhrung einer Kette von Operationen fĂĽr jeden Stream dasselbe Ergebnis liefern, fĂĽr ungeordnete Streams muss dies nicht der Fall sein, z.B. muss IntStream.range(0, 3).unordered().map(x -> x*2).collect(Collectors.toList()) bei wiederholter AusfĂĽhrung nicht unbedingt immer [0, 2, 4] erzeugen, sondern es wäre fĂĽr jede AusfĂĽhrung jede Permutation der resultierenden Listenelemente erlaubt. Diese Ordnung zu garantieren, ist fĂĽr sequentielle Streams fast immer viel einfacher als fĂĽr parallele!

Eigenschaften von Stream-Operationen

Intermediär oder Terminal â€“ Auf einem Stream können beliebig viele intermediäre Operationen zum Einsatz kommen und maximal eine terminale Operation. Intermediäre Operationen wie map(), filter(), distinct(), sorted() usw. geben daher wieder einen Stream zurĂĽck, auf dem dann durch Aufruf der Fluent-API-Methoden weitere Operationen ausgefĂĽhrt werden können. Terminale Operationen wie reduce()collect(), findFirst(), findAny() usw. konsumieren ihren Stream indem sie alle intermediären Operationen ausfĂĽhren und dann ein endgĂĽltiges Ergebnis (keinen Stream) liefern.

Short-circuiting– Stream-Operationen sind abkĂĽrzend, wenn sie einen Stream “verkĂĽrzen”. Sie können intermediär sein wie skip()und limit() oder terminal wie findFirst() und findAny().

Stateless oder Stateful â€“ Manche intermediären Stream-Operationen mĂĽssen einen internen Zustand halten, um ihr Ergebnis zu berechnen. Beispiele dafĂĽr sind limit(), skip(), distinct(), sorted(). Manche dieser Operationen, z.B. distinct() und sorted(), die den ganzen Stream sehen mĂĽssen, um ein korrektes Ergebnis zu liefern, funktionieren daher generell nicht besonders gut auf parallelen Streams (s.o.). limit() und skip() sind nur dann problematisch, wenn ein geordneter Stream parallel bearbeitet werden soll, da es dann wichtig ist, welche Elemente einbezogen bzw. ĂĽbersprungen werden.

Seiteneffekte â€“ Prinizipiell kann jede Stream-Operation Seiteneffekte erzeugen, indem sie auĂźerhalb ihrer Lambda-Scopes liegende Objekte verändert, was allerdings generell nicht zu empfehlen ist. z.B.:

List<String> stringList = new ArrayList<>();
points.stream().forEach(p -> results.add(p.toString())); // ARGH!

Dies ist ein vermeidbarer Seiteneffekt. Anstelle von forEach(Consumer) (was im ĂĽbrigen auch fĂĽr geordnete Streams die Reihenfolge im Gegensatz zu forEachOrdered(Consumer) nicht einhalten muss!) sollte hier besser collect(Collector) verwendet werden:

List<String> stringList = points.stream()

        .map(Object::toString)         // or: p -> p.toString()

        .collect(Collectors.toList());

Ein nicht vermeidbarer Seiteneffekt ist z.B. die Ausgabe auf der Console, bzw. generell das Schreiben in Datensenken. AuĂźerdem gibt es in der Kategorie noch die intermediäre Operation peek(Consumer):

List<Point> positiveX = points.stream()

        .peek(System.out::println) // all

.filter(p -> p.x > 0)

.peek(p -> System.out.println(„** “ + p)) // only filtered

.collect(Collectors.toList());

Das peek()-Beispiel zeigt übrigens auch sehr schön, in welcher Reihenfolge die Operationen tatsächlich ausgeführt werden, man achte dazu auf die Position der mit “**” beginnenden Ausgaben, der aus der Liste [ Point(1,1), Point(1,-1), Point(-1,-1), Point(-1,1) ] herausgefilterten Punkte mit positiver X-Koordinate:

java.awt.Point[x=1,y=1]

** java.awt.Point[x=1,y=1]

java.awt.Point[x=1,y=-1]

** java.awt.Point[x=1,y=-1]

java.awt.Point[x=-1,y=-1]

java.awt.Point[x=-1,y=1]

Wie man sieht, werden in einem sequentiellen Stream zunächst fĂĽr jedes Listen-Element alle relevanten Stream-Operationen ausgefĂĽhrt bevor mit dem nächsten Listen-Element fortgefahren wird. Weiterhin finden die Ausgaben nur statt, wenn eine terminale Operation wie hier collect() in der Pipeline vorhanden ist.

Reduce vs. Collect oder Immutability vs. Mutability

Die Operation collect() im vorigen Beispiel ist eigentlich wenig “funktional”, da sie eine durch Collectors.toList() intern) initial erzeugte Liste durch die Operation verändert, nämlich mit Werten befĂĽllt.

Im Gegensatz dazu geht reduce() von nicht veränderlichen (immutable) Objekten aus, die schrittweise von der eigentlichen Reduktionsfunktion (z.B. Aufsummierung, Konkatenierung) zu neuen nicht veränderlichen Objekten und schlieĂźlich zum Ergebnis zusammengefĂĽgt werden.

Beispiele von String-Repräsentationen

Folgende Beispiele zum Zusammenfügen von String-Repräsentationen (Strings sind in Java unveränderliche Objekte) der Elemente der Punkteliste aus den vorangegangen Beispielen verdeutlichen den Unterschied:

1. String-Konkatenation

String pointsStr = points.stream().reduce(„“,     // Initial value

(String s, Point p) -> s + “ “ + p,         // Accumulator

(String s1, String s2) -> s1 + s2));        // Combiner

Die Kombinationsfunktion (Combiner) benötigt man nur für parallele Ausführung, da hier die Reduktion zunächst auf Teillisten erfolgt und die Teilergebnisse anschließend kombiniert werden müssen.

NatĂĽrlich weiĂź schon ein Java-Anfänger, dass String-Konkatenation keine besonders performante Operation ist. Der Compiler optimiert bei langen “+”-Ketten zwar durch interne Benutzung von StringBuilder, aber in diesem Fall ist das nur innerhalb der Lambdas fĂĽr die Einzelteile möglich, so dass hier im Endeffekt anstelle vieler String-Objekte viele StringBuilder-Objekte erzeugt und wieder verworfen werden. Besser wäre vielleicht dieser Ansatz:

2. Reduce mit StringBuilder

String pointsStr = points.stream().reduce(new StringBuilder(),

StringBuilder::append,     // (sb, point) -> sb.append(point)

StringBuilder::append)    // (sb, sb) -> sb.append(sb)

.toString();                // make String from StringBuilder

Nun werden dem initialen Wert, einem neuen StringBuilder-Objekt mittels append() die String-Repräsentationen der Listen-Elemente der Reihe nach hinzugefügt. Das funktioniert allerdings nur, weil append() eine “Funktion” ist, sie gibt nämlich als Ergebnis den (veränderten) StringBuilder wieder zurück. Außerdem funktioniert dieser Ansatz nur sequentiell! Denn wenn derselbe(!) initiale Wert in parallel ausgeführten Verarbeitungsschritten (points.parallelStream().reduce(…)) durch append() verändert wird, führt das mindestens dazu, dass höchstwahrscheinlich die Ordnung der Liste nicht eingehalten wird, aber in der Regel endet dieser Versuch mit einer Exception, da StringBuilder-Objekte nicht threadsafe sind.

FĂĽr solche Fälle gibt es in Java 8 daher collect() â€“ auch “mutable reduction” genannt:

3. Collect mit StringBuilder

String pointsStr = points.parallelStream()

.collect(StringBuilder::new,

StringBuilder::append, StringBuilder::append)

.toString();

Der Unterschied zu reduce() ist, dass hier als Startwert gar kein Datenwert sondern eine Funktion (Lambda Ausdruck oder Methodenreferenz) erwartet wird, die fĂĽr jeden parallelen Verarbeitungsteil das eigentliche (veränderliche) Startobjekt erzeugt (Supplier). Hier startet also jeder parallele Teil mit einem eigenen StringBuilder. Des Weiteren sind Akkumulator und Combiner hier keine BiFunctions, sondern BiConsumers, die keine RĂĽckgabewerte erwarten bzw. RĂĽckgabewerte, wie etwa von append(), verwerfen.

Die Klasse Collectors stellt viele nĂĽtzliche Collector-Implementierungen zur Benutzung mit der Stream-Methode collect(Collector) bereit. Collector-Objekte fassen die o.g. Akkumulations- und Kombinationslogik zusammen.

Als einfĂĽhrende und weiterfĂĽhrende Literatur zu Streams seien als zwei von vielen diese Quellen genannt:

  • Kurz und bĂĽndig: Die Package-Beschreibung von java.util.stream.
  • AusfĂĽhrlich: Richard Warburton, “Java 8 Lambdas – Functional Programming for the Masses”, O’Reilly

Des Weiteren danke ich Angelika Langer und Klaus Kreft für einen inspirierenden Lambda- und Streams-Hackathon am 6.11.2014 in Berlin, aus dem das eine oder andere Beispiel in veränderter Form für diesen Artikel verwendet wurde.

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”).

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.

Schwachpuntke Java 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.

Veröffentlicht am 24. November 2014, aktualisiert am 18. Oktober 2020

Teilen auf

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”.

SAP Silver Partner Logo

Neofonie setzt auf SAP Commerce Cloud

Als Spezialist für E-Commerce Lösung hat Neofonie die Partnerschaft mit SAP ausgeweitet und ist nun auch Implementierungspartner für die SAP Commerce Plattform.

Illustration von CRM-Systemen

Neofonie hilft mit CRM-Beratung zur 360° Kundensicht

Neofonie unterstĂĽtzt Sie beim Aufbau eines erfolgreichen Customer Relation Managements.

SEO in bunten Buchstaben

Webinar: Relaunch und SEO – So machen Sie es richtig

Am 25.08. zeigt SEO-Experte Anton Littau, wie die richtige Strategie zum richtigen Zeitpunkt hilft.

SAP Silver Partner Logo

Neofonie ist SAP Silver Partner mit TXTWerk

Als SAP Silver Partner stellt Neofonie das Text Mining Framework TXTWerk im SAP App Center ab sofort bereit.

Ibexa-Logo

Neofonie und Ibexa sind Partner

Neofonie und Ibexa vereinbaren Partnerschaft, um Content und Commerce stärker miteinander zu verbinden.

Grant McGillivray

Neofonie startet eigenständige UX-Agentur

Das Geheimnis ist nun endgĂĽltig gelĂĽftet: ION ONE nennt sich die neue UX-Agentur von Neofonie!

digital-ratgeber.de auf einen Macbook

Wort und Bild Verlag stellt Digital Ratgeber vor

Mit Neofonie als Partner bringt der Wort & Bild Verlag mit dem „Digital Ratgeber“ eine neue Informationsplattform für Apotheken und deren Kunden auf den Markt.

Stadtwerke Oranienburg News

Storytelling-Beratung fĂĽr Stadtwerke Oranienburg

Neofonie unterstĂĽtzt die Stadtwerke Oranienburg in einem kreativen Kommunikationsworkshop, um ihr Online-Ableseportal mit einem Kino-Werbespot zu promoten.

Frau mit Kind im Arm, tröstet das Kind

Spende fĂĽr ein Berliner Kinderhospiz

Mitarbeiter der Neofonie und Neofonie Mobile spenden 1.500 EUR für das Berliner Kinderhospiz „Berliner Herz“.

Niedlicher Roboter

Chatbot erstellen in 5 Schritten

Wie ein Chatbot in wenigen Schritten entwickelt werden kann, stellen Bertram Sändig und Cornelia Werk vor.

Schwarzes Mikrofon

Neofonie Blogcast jetzt auf Google Podcast anhören

Mit der Einführung von Google Podcast in Deutschland, ist jetzt auch der Neofonie Blogcast auf allen Google-Geräten verfügbar.

Wann Big Data im Forderungsmanagement sinnvoll ist

Ob Analyseverfahren fĂĽr ein Unternehmen sinnvolle Ergebnisse bringen, kann nicht pauschal beantwortet werden.

Rot-beleuchtete Treppen

Flexibilität als Erfolgsfaktor bei der Shopsystem Auswahl

Welche Punkte Sie bei einer neuen Shoplösung beachten sollten, stellt Michael Vax, VP Product bei Spryker vor.

Jovo Logo

Neofonie Mobile baut Alexa Skills and Google Actions mit Jovo

Neofonie Mobile ist neuer Partner des Entwicklungs-Framework Jovo und erstellt damit Sprachanwendungen fĂĽr Google und Alexa.

Illustration von digitalen Versicherungen

Versicherungen im Vergleich: Apps sind Pflicht

Die aktuelle Marktanalyse des App-Spezialisten Neofonie Mobile belegt Nachholbedarf in der digitalen Transformation unter Versicherern und Krankenkassen.

Text Mining Consulting KI E-Commerce

Wie Onlineshops von KĂĽnstlicher Intelligenz profitieren

Welche Punkte Sie bei einer neuen Shoplösung beachten sollten, stellt Michael Vax, VP Product bei Spryker, vor.

Was eine moderne Suchlösung leisten muss

Eine Suche auf Websites hilft Nutzern, schneller Informationen zu finden. Suchlösungen haben dabei nicht mehr länger nur den Anspruch einzelne Begriffe zu finden, sondern relevante Inhalte gezielt auf die Bedürfnisse des Nutzers auszuspielen.

Individual-Commerce: Wie Produktkonfiguratoren Onlineshopper glĂĽcklich machen

Auch Onlineshops können den Kunden mittels smarter Produktkonfiguratoren individuell abholen und ihnen ihr ganz persönliches Shoppingerlebnis ermöglichen.

Jägermeister Produktkonfigurator

Wie Jägermeister mit neuem Onlineshop mehr Umsatz macht

Das Erfolgsrezept stellt Wiljo Krechting, Manager Public Relations von Shopware, genauer vor.

Mit B2B-Storytelling und UX zu mehr Erfolg

B2B Unternehmen mĂĽssen vom B2C-Nutzererlebnis lernen, um ihre Businesskunden nachhaltig zu begeistern. Dabei stehen User Experience (UX) und Storytelling im Mittelpunkt.

Unser Newsletter „Neo Report“ vermittelt konkretes Praxiswissen, Trends und Know-how für Ihr digitales Business – quartalsweise und kompakt. Jetzt anmelden.