Arnd KleinbeckInnovation und Technologie

Lambdas und das neue Stream API in Java 8

Die Entwicklergemeinde bezeichnet die Einführung von Lambdas in Java 8 zu Recht als eine echte Revolution. Seit der Einführung der Generics mit Java 1.5 hat es keine Neuerung im Sprachumfang gegeben, die bereits im Vorfeld der Veröffentlichung zu ähnlich hoher Begeisterung geführt hat.

Funktionale Programmierung

Lambda-Ausdrücke oder auch Closures sind in der Software-Entwicklung schon seit längerem als Konstrukte funktionaler Programmiersprachen bekannt. Bereits in den 1960er Jahren entwickelt, wurde diese durch den Siegeszug der Objektorientierung zunächst stark in den Hintergrund gedrängt. Mit der aufkommenden Popularität von Sprachen wie Scala, Scheme und Closure erleben sie seit einigen Jahren eine bemerkenswerte Renaissance. Das hängt nicht zuletzt mit der besseren Eignung des funktionalen Ansatzes für die effiziente Nutzung von Multicore-CPUs zusammen, die heutzutage zum gängigen Standard geworden sind.

Über den JSR 335 (Lambda Expressions for the Java Programming Language) halten diese bewährten funktionalen Konzepte nun auch Einzug in Java, das bis dato als eine rein imperative Sprache auf dem Paradigma der Objektorientierung basierte.

Implizite vs. explizite Iteration

Vor Java 8 war es notwendig, bei der Verarbeitung der Elemente einer Menge von Objekten eine explizite Iteration durchzuführen. Hat man das jeweilige Element im Zugriff, können darauf die entsprechenden Operationen ausgeführt werden. Für den beispielhaften Aufruf der Methode draw() auf allen Elementen einer Menge von Glyph-Instanzen sieht die explizite Iteration folgendermaßen aus:

Collection<Glyph> glyphs = ...;
for (Glyph glyph: glyphs) {
  glyph.draw();
}

Die neuen Collection-Klassen ermöglichen nun durch die Erweiterung des Iterable-Interfaces um die Methode forEach im Gegensatz zum vorherigen Beispiel eine implizite Iteration. Dabei wird die eigentliche Iterationslogik und damit auch die Verantwortung für dessen Parallelisierung vom Client-Code in den Bibliotheks-Code verlagert.

Als Übergabeparameter erhält die forEach-Methode einen Lambda-Ausdruck, der die pro Mengenelement durchzuführende Operation definiert:

Collection<Glyph> glyphs = ...;
glyphs.forEach(glyph -> glyph.draw());

Bei gleichbleibendem Verhalten kommt man hier ohne Schleife aus, was nicht nur die Komplexität des Codes reduziert, sondern auch dessen Lesbarkeit und Ausdrucksstärke verbessert.

Die Syntax eines Lambda-Ausdrucks ist folgendermaßen definiert:

        (parameters) -> expression

Im Beispiel wird der Parameter des Lambda-Ausdrucks durch ein Element der Menge definiert. Hinter dem Pfeil steht die Operation, die auf jedem Element aufgerufen werden soll.

Das Stream API

Neben der beschriebenen forEach-Methode für die Collection-Klassen bietet das Stream API noch eine Vielzahl weiterer Funktionen, die mit Lambda-Ausdrücken umgehen können. Diese ermöglichen einen eleganten Programmierstil, der stark an das Pipes And Filters-Konzept der Unix-Kommandozeile erinnert. Man kann über eine Verkettung von Funktionen eine Art Pipeline aufbauen, durch die die einzelnen Objekte des Streams während der Ausführung hindurchwandern. Ein wesentliches Prinzip von Streams besteht darin, dass die zugrundeliegenden Quell-Datenstrukturen niemals durch die Stream-Operationen verändert werden. Vielmehr werden Kopien der Elemente erstellt, die schließlich nach Durchlaufen der Pipeline in transformierter Form in der Ergebnismenge landen.

Der folgende Code-Ausschnitt zeigt eine Stream-Verarbeitung, bei der aus einer Menge von Formen zuerst all diejenigen herausgefiltert werden, die einen Kreis darstellen. Nur auf diesen Objekten wird die nachfolgende Funktion aufgerufen, die den Flächeninhalt der jeweiligen Form berechnet. Schließlich erfolgt durch die Aggregatsfunktion sum() eine Addition aller Flächeninhalte.

        Collection<Glyph> glyphs = …;

        float sum = glyphs.stream()

               .filter(glyph -> glyph.getShape() == Shape.CIRCLE)

               .map(glyph -> glyph.calculateArea())

               .sum();

Effiziente Nutzung von Multicore-CPUs

Achtet man darauf, dass die verwendeten Methoden und Lambda-Ausdrücke seiteneffektfrei sind, so sind die Voraussetzungen für eine einfache Parallelisierung der Berechnung gegeben, ohne dass das Ergebnis dadurch beeinflusst wird.

Das vorhergehende Code-Beispiel stellt genau solch einen Fall dar: Die Ermittlung der Flächeninhalte einzelner Formen ist unabhängig voneinander und bei der Berechnung der Gesamtsumme kommt es nicht auf die Reihenfolge der einzelnen Summanden an.

Für den Entwickler ist unter den genannten Voraussetzungen die Ausnutzung aller zur Verfügung stehender CPU-Cores denkbar einfach: Durch den Aufruf der Operation parallel() auf der Stream-Instanz wird die interne Parallelverarbeitung für die Stream-Verarbeitung aktiviert.

        Collection<Glyph> glyphs = …;

        float sum = glyphs.stream().parallel()

              .filter(glyph -> glyph.getShape() == Shape.CIRCLE)

              .map(glyph -> glyph.calculateArea())

              .sum();

Eine einfache Messung und Gegenüberstellung der CPU-Auslastung bei sequenzieller und paralleler Stream-Verarbeitung auf identischen Quelldaten zeigt sehr anschaulich, dass nicht nur alle verfügbaren CPU-Cores effizient ausgenutzt werden, sondern dass auch die Gesamtlaufzeit der Berechnung im Falle der Parallelverarbeitung deutlich reduziert ist.

activitymonitor Lambda-beitrag

Fazit

Ganz besonders das Java Collections Framework, das ja mittlerweile über 15 Jahre alt ist, profitiert von den neuen funktionalen Ansätzen in Java 8. Die Verwendung des „Pipes and Filters“-Patterns bringt neben der prägnanteren Ausdrucksweise signifikante Vorteile wie den Wegfall von Zwischenwert-Variablen, die Reduktion von benötigtem Speicherplatz und die flexiblere Komponierbarkeit unterschiedlicher Operationen.

Nicht zuletzt ist die einfache Parallelisierbarkeit der Stream-Verarbeitung ein echtes Killer-Feature, das besonders vor dem Hintergrund der heutigen Multicore-Prozessoren einen wirklich revolutionären Fortschritt darstellt.

Schlagwörter: ,