Java 8 – Stream API

Kommen wir nun im dritten Artikel zu den etwas spannenderen und vielleicht nicht ganz so theoretischen und ersten Annäherungen, die Java8 (oder 1.8) an Neuheiten mit sich bringt – die Stream API.

Das Beispiel aus dem Artikel zu Functional Interfaces wird hierbei als Grundlage verwendet ist aber natürlich als eigenes Package java_8_class_3 im Github-Repository zu finden. Kurz zusammengefasst wird hier die neue forEach-Methode des Iterable Interfaces genutzt, um über alle Elemente der Liste zu iterieren. Statt direkt auf der Liste zu iterieren, werden wir uns im ersten Schritt einen Stream erzeugen und diesen ausgeben:

Auch wenn uns der Optimizer darauf hinweist, dass wir den Ausdruck durch Verwendung der Collections.forEach-Methode vereinfachen können, ist unser Code lauffähig und erzeugt für diese Anweisung folgende Ausgabe:

Dass dieses Beispiel nicht sinnvoll ist – klar und dass die Verwendung von Stream hier nicht notwendig ist, ist auch klar, aber werfen wir einen Blick auf folgendes Beispiel:

Folgende Ausgabe erscheint:

Wir filtern also alle Strings, deren Werte kleiner als 4 sind. Nur der Vollständigkeit halber, können wir dieses Statement auch so schreiben:

Das ganze ist nicht nur deutlich unübersichtlicher und länger, sondern ist auch in der eigentlichen Entwicklung deutlich unangenehmer.

Um den Umgang mit der Stream API zu beherrschen sollten einige Dinge bekannt sein, ohne die es wenig Spaß macht, diese zu verwenden. Beginnen wir mit drei verschiedenen Operationskategorien, die auf Streams angewendet werden können:

  1. Create-Operationen:
    • meineListe.stream() –> sequentiell verarbeiteter Stream auf Basis einer Collection
    • meineListe.parallelStream() –> parallel verarbeiteter Stream auf Basis einerCollection
    • Arrays.stream(meinArray)[.parallel()] –> sequentiell oder parallel verarbeiteter Stream auf Basis eines Arrays
    • Stream.of(String-values oder Integer-values) –> Direkte Erstellung auf Basis von übergebenen Werten
    • IntStream.range(beginInt,endInt) –>Erzeugung eines IntStream mit Range von 7 bis 42, wobei das untere Ende des Intervals enthalten und die obere Grenze nicht enthalten ist
    • Stream<Path> kann durch die die Verwendung von Files.list() auf einem Verzeichnis etabliert werden
    • Files.lines() liest einen Stream<String> Textdatei
  2. Intermediate-Operationen:
    • filter() –> Aussortieren von Elementen auf dem Stream; Verwendung von .filter().filter() ist genauso möglich wie die Verwendung von .filter( (Bedingung).and(Bedingung))
    • distinct() –> Entfernung doppelter Einträge
    • sorted() –> Sortierung
    • skip(n) –> Überspringen der ersten n Elemente
    • limit(n) –> Begrenzung auf n Elemente
    • map() –> Mapping eines bestimmten Element(Typs) zu einem anderen Element(Typ); die Typumwandlung ist hierbei optional, wird aber sehr häufig genutzt
    • peek() –> Untersuchung des aktuellen Elements ohne ein Fortsetzen der Iteration
  3. Terminal-Operationen:
    • forEach() –> Behandlung jedes Elements des Stream
    • collect(Collectors.toList()) –> Alle Elemente des verarbeiteten Streams werden in eine Liste gespeichert
    • collect(Collectors.toMap(myElem::id,myElem::name)) –> Speicherung der aus dem Stream resultierenden Daten in eine Map<Integer,String)
    • collect(Collectors.groupingBy()) –> Gruppierung der Elemente
    • reduce() –> Reduzierung auf einen einzelnen Wert anhand des gegebenen Ausdrucks
    • anyMatch() –> Reduzierung auf einen boolschen Wert anhand des gegeben Predicates
    • findFirst() –> Reduzierung auf das erste Element gekapselt in einem Optional

Es gibt sehr viel mehr Beispiele als die, die hier aufgelistet sind. Gerade im Bereich der Collectors ist noch sehr viel Redebedarf ;-). Eine weitere wichtige Information ist, dass Streams lazy sind, das bedeutet, dass erst mit dem Hinzufügen einer Terminal-Operation die angegebenen Operationen auf einem Stream ausgeführt werden.

ZeroTurnaround hat einen brauchbaren Cheatsheet für den Einstieg veröffentlicht.

Java 8 – Lambdas (Closures)

Während in Java häufig nur von Lambdas gesprochen wird, handelt es sich bei Lambdas um die in Java umgesetzte Variante von Closures, deswegen sollten wir hier einen kurzen Blick auf Closures werfen.

Was ist eine Closure? Eine Closure – ja das Wort ist feminin – ist ein Konstrukt aus der Softwareentwicklung, bei der eine Funktion Zugriff auf seinen Erstellungskontext hat – hää wie jetzt? Na gut, etwas mehr Info anhand eines Beispiels (Quelle des Beispielcodes):

In dem Beispiel (zum Github-Repository) gibt es eine Funktion make_fun(), diese hat als Rückgabe eine anonyme Funktion, hierfür ist Lambda ein Synonym, häufig fällt in diesem Kontext gerade bei JavaScript und PHP der Begriff des Callbacks. Wie zu sehen ist, kann die zurückgegebene Funktion (der Teil in den geschweiften Klammern) auf die lokale Variable n zugreifen und diese sogar verändern, obwohl sie außerhalb des eigentlichen Funktionskontextes liegt, dem sogenannten Erstellungskontext. Die Funktion make_fun hat in der Methodensignatur keinen Funktionsparameter und einen Rückgabewert vom Typ Function<Integer, Integer>, was zugegeben auch zu erwarten war, da wir eine anonyme Funktion erstellen. Ein Blick hinter die Kulissen des Typs zeigt:

Wobei T der Typ des Parameters für die Funktion und R der Typ des Rückgabewertes der Funktion ist. Die Schreibweise ist etwas gewöhnungsbedürftig für Neulinge im Feld der funktionalen Programmierung, aber nach kurzer Eingewöhnungsphase doch verständlich. Die Invocation unserer Funktion findet anschließend durch den Aufruf der apply-Methode statt. Hierbei wird die Methode auf die übergebenen Werte angewendet – beispielhaft mit 5 Werten in zwei Durchläufen.

Zu sehen ist, dass sowohl der Callback auf der Variablen x als auch auf der Variablen y ihre eigene Variable n haben (wovon wir auch ausgegangen sind), sich der Inhalt der Variablen aber nicht ändert. Sollte nun versucht werden, den Wert von n innerhalb des Callbacks zu verändern, beispielsweise durch n += arg;, so wirft der Compiler (JIT Compiler) direkt einen Fehler:

Das bedeutet, dass die in Java verfügbaren Closures in Form von Lambdas keine Variablen sondern Wert geschlossenen Closures sind (Zur Erinnerung eine Closure ist ein über Speicherstruktur und Funktion geschlossenes Konstrukt).

Wenn eine Variable verändert werden muss, kann dies durch ein auf dem Heap abgelegtes Objekt etabliert werden:

Bringen wir diesen Code nun mit unseren beiden Beispielen zur Ausführung erhalten wir folgendes Ergebnis, in dem wir auch die außerhalb der Funktion gelagerten Variablen bearbeiten können, da der Compiler hier lediglich die Änderung der Referenz innerhalb der Funktion prüft (probiere einfach, myWrapperClass innerhalb der anonymen Methode neu zu instanziieren):

Wenn man hierbei noch Concurrency-Probleme beachtet (mutable shared state), hat man durch einen kleinen Umweg genau das, was ein Closure ausmacht.

Trotzdem sind Lambdas sicherlich eine sehr sinnvolle Neuerung in Java8.

Der Programmcode kann im Github-Repository gecloned werden. Der relevante Beispielcode findet sich im Package java_8_class_2.

Java 8 – Functional Interfaces

Ich bin mir oft nicht sicher, ob der Hype um das Thema funktionale Programmierung übertrieben ist, doch ich habe erlebt, dass einige Operationen kürzer, schneller und übersichtlicher implementiert werden können und beispielsweise mit parallelen Streams auch noch schneller verarbeitet werden können – vorausgesetzt eine parallele Verarbeitung ist möglich. Zudem denke ich, dass kein Entwickler, wirklich um ein grundsätzliches Verständnis der funktionalen Programmierung drum herum kommt, da viele Kunden und Auftraggeber häufig von Buzzwords getrieben sind und der Meinung sind, dass eine moderne Software auf einem funktionalen Programmieransatz beruht – ich sehe dies wie viele meiner Kollegen etwas anders, die sagen…“Kommt drauf an“…

Doch nun genug Stimmungsmache ;-):

Im ersten Code-Snippet werden wir mit Hilfe der neuen default-Implementierung einer forEach-Methode aus dem allgemein bekannten Iterable-Interface:

In dem ersten Beispiel im Kontext Java 8 wird über eine Liste von Strings iteriert und die Char-Sequence ausgegeben. Dazu habe ich hier ein Github-Repository mit den Beispielen angelegt, über die wir hier sprechen werden. Betrachtet wird die Klasse Java_8_Class_1. Bisher verwendeten wir die in Java 1.5 eingeführte extended-Loop:

Das Interface Iterable bringt nun die forEach Methode mit, die als Parameter ein Consumer<T> als Parameter erwartet:

Die Ausgabe von Java_8_Class_1 sollte wie nachfolgend dargestellt sein: