Lambda-Ausdrücke und Methodenreferenzen

Effective Java: Let’s Lambda!


Wie bereits im letzten Beitrag unserer Reihe gesehen, gibt es in Java 8 neue Sprachmittel, die einen eher funktionalen Programmierstil in Java unterstützen werden. Diese neuen Sprachmittel sind die Lambda-Ausdrücke und die Methodenreferenzen. Damit wollen wir uns in diesem Beitrag näher befassen.

Diskussionen über Spracherweiterungen in Java für funktionale Programmierung hat es schon vor einigen Jahren gegeben. Seit der Freigabe von Java 5 wurde intensiv darüber nachgedacht, wie solche Erweiterungen aussehen könnten. Es gab drei konkrete Vorschläge in dieser als „Closure-Debatte“ bekannt gewordenen Anstrengung [1]. Neil Gafter, vormals als Compilerexperte bei Sun tätig, hatte sogar einen Prototyp-Compiler für den Vorschlag gebaut, an dem er mitgewirkt hatte. Dennoch hat sich keine Konvergenz der drei Closure-Vorschläge ergeben. Es hatte auch keiner der drei Vorschläge die uneingeschränkte Unterstützung von Sun Microsystems. Als Sun Microsystems dann auch noch von Oracle übernommen wurde, verlief die Closure-Diskussion ergebnislos im Sande. Es sah zunächst so aus, als würde es in Java keine Erweiterungen für die funktionale Programmierung geben.

Im Jahr 2009 setzte sich dann die Erkenntnis durch, dass Java ohne Closures (oder Lambdas, wie sie fortan hießen) gegenüber anderen Programmiersprachen veraltet aussehen könnte. Erstens gibt es Closure- bzw. Lambda-artige Sprachmittel in einer ganzen Reihe von Sprachen, die auf der JVM laufen. Zweitens braucht man auf Multi-CPU- und Multi-Core-Hardware eine einfache Unterstützung für die Parallelisierung von Programmen. Denn was nützen die vielen Cores, wenn die Applikation sie nicht nutzt, weil sie in weiten Teilen sequenziell und nur in geringem Umfang parallel arbeitet?

Nun bietet das JDK mit seinen Concurrency Utilities im java.util.concurrent-Package umfangreiche Unterstützung für die Parallelisierung. Die Handhabung dieser Concurrency Utilities ist aber anspruchsvoll, erfordert Erfahrung und wird allgemein als schwierig und fehleranfällig angesehen. Eigentlich bräuchte man für die Parallelisierung bequemere, weniger fehleranfällige und einfach zu benutzende Mittel. Doug Lea, der sich schon seit vielen Jahren um die Spezifikation und Implementierung der Concurrency Utilities in Java kümmert, hat dann prototypisch eine Abstraktion ParallelArray gebaut, um zu demonstrieren, wie eine Schnittstelle für die parallele Ausführung von Operationen auf Sequenzen von Elementen aussehen könnte [2]. Die Sequenz war einfach ein Array von Elementen mit Operationen, die paralleles Sortieren, paralleles Filtern sowie das parallele Anwenden von beliebiger Funktionalität auf alle Elemente der Sequenz zur Verfügung gestellt hat. Dabei hat sich herausgestellt, dass eine solche Abstraktion ohne Closures bzw. Lambdas nicht gut zu benutzen ist.

Deshalb gibt es seitdem bei Oracle unter der Leitung von Brian Goetz (der vielen Lesern vielleicht als Autor des Buchs „Java Concurrency in Practice“ bekannt ist) ein „Project Lambda“, d. h. eine Arbeitsgruppe, die die neuen Lambda-Sprachmittel definiert und gleichzeitig neue Abstraktionen für das JDK-Collection-Framework spezifiziert und implementiert hat [3]. Ein ParallelArray wird es in Java 8 zwar nicht geben; das war nur ein Prototyp, der Ideen lieferte. An seine Stelle treten so genannte Streams. Und aus dem anfänglich als Closure bezeichneten Sprachmittel sind im Laufe der Zeit Lambda-Ausdrücke sowie Methoden- und Konstruktorreferenzen entstanden. Diese Lambda-Ausdrücke bzw. Methoden-/Konstruktorreferenzen wollen wir uns im Folgenden genauer ansehen [4].

Wie sieht ein Lambda-Ausdruck aus?

Wir haben im letzten Beitrag bereits Lambda-Ausdrücke gezeigt, und zwar am Beispiel der Verwendung der forEach-Methode. In Java 8 haben alle Collections eine forEach-Methode, die sie von ihrem Superinterface Iterable erben. Das Iterable-Interface gibt es schon seit Java 5; es ist erweitert worden und sieht in Java 8 so aus:

public interface Iterable<T> { Iterator<T> iterator(); default void forEach(Consumer<? super T> action) { for (T t : this) { action.accept(t); } } }

Das Iterable-Interface hat zusätzlich zur iterator-Methode, die es schon immer hatte, eine forEach-Methode bekommen. Die forEach-Methode iteriert über alle Elemente in der Collection und wendet auf jedes Element eine Funktion an, die der Methode als Argument vom Typ Consumer übergeben wird. Die Benutzung der for­Each-Methode sieht dann zum Beispiel so aus:

List<Integer> numbers = new ArrayList<>(); ... populate list ... numbers.forEach(i -> System.out.println(i));

Als Consumer haben wir einen Lambda-Ausdruck übergeben (i -> System.out.println(i)), der alle Integer-Werte aus der Collection nach System.out ausgibt.

Ein Lambda-Ausdruck besteht aus einer Parameterliste (das ist der Teil vor dem „->“-Symbol) und einem Rumpf (der Teil nach dem „->“-Symbol). Für Parameterliste und Rumpf gibt es mehrere syntaktische Möglichkeiten. Hier die vereinfachte Version der Syntax für Lambda-Ausdrücke:

LambdaExpression: LambdaParameters '->' LambdaBody LambdaParameters: Identifier '(' ParameterList ')' LambdaBody: Expression Block

Lambda-Parameterliste

Die Parameterliste ist entweder eine kommagetrennte Liste in runden Klammern oder ein einzelner Bezeichner ohne runde Klammern. Wenn man die Liste in Klammern verwendet, dann kann man sich entscheiden, ob man für alle Parameter den Parametertyp explizit hinschreiben will oder ob man den Typ weglässt und ihn vom Compiler automatisch bestimmen lässt. Tabelle 1 zeigt ein paar Beispiele.

(int x) -> x+1

Parameterliste mit einem einzigen Parameter mit expliziter Typangabe

int x -> x+1

Falsch: Wenn man den Parametertyp angibt, muss man die runden Klammern verwenden

(x) -> x+1

Parameterliste mit einem einzigen Parameter ohne explizite Typangabe; der Compiler deduziert den fehlenden Parametertyp aus dem Kontext

x -> x+1

Parameterliste mit einem einzigen Parameter ohne explizite Typangabe, in diesem Fall darf man die runden Klammern weglassen

(int x,int y) -> x+y

Parameterliste mit zwei Parametern mit expliziter Typangabe

int x,int y -> x+y

Falsch: Wenn man den Parametertyp angibt, muss man die runden Klammern verwenden

(x,y) -> x+y

Parameterliste mit zwei Parametern ohne explizite Typangabe

x,y -> x+y

Falsch: Bei mehr als einem Parameter muss die runde Klammer verwendet werden

(x,int y) -> x+y

Falsch: Man darf Parameter mit und ohne Typangabe nicht mischen, entweder alle haben eine explizite Typangabe oder keiner

() -> 42

Die Parameterliste darf leer sein

Tabelle 1: Beispiele für Parameterlisten

Lambda-Body

Der Rumpf ist entweder ein einzelner Ausdruck oder eine Liste von Anweisungen in geschweiften Klammern....

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang