Lambda-Ausdrücke haben wie Methoden mögliche Parameter- und Rückgabe-Werte. Die Java-Grammatik für die Schreibweise von Lambda-Ausdrücken sieht ein paar nützliche syntaktische Abkürzungen vor.
Ausführliche Schreibweise
Lambda-Ausdrücke lassen sich auf unterschiedliche Arten und Weisen schreiben, da es für diverse Konstruktionen Abkürzungen gibt. Eine Form, die jedoch immer gilt ist:
‚(‚ LambdaParameter ‚)‘ ‚->‘ ‚{‚ Anweisungen ‚}‘
Der Lambda-Parameter besteht (voll ausgeschrieben) wie ein Methodenparameter aus a) dem Typ, b) dem Namen und c) optionalen Modifizieren.
Der Parametername öffnet einen neuen Gültigkeitsbereich für eine Variable, wobei der Parametername keine anderen Namen von lokalen Variablen überlagern darf. Hier verhält sich die Lambda-Parametervariable wie eine neue Variable aus einem inneren Block und nicht wie eine Variable aus einer inneren Klasse, wo die Sichtbarkeit anders ist.
Beispiel
Folgendes gibt einen Compilerfehler im Lambda-Ausdruck, weil var schon deklariert ist, die Parametervariable vom Lambda-Ausdruck muss also „frisch“ sein:
String var = ""; var.chars().forEach( var -> { System.out.println( var ); } ); // Compilerfehler
Abkürzung 1: Typinferenz
Der Java-Compiler kann viele Typen aus dem Kontext ablesen, was Typ-Inferenz genannt wird. Wir kennen so etwas vom Diamant-Operator, wenn wir etwa schreiben List<String> list = new ArrayList<>().
Sind für den Compiler genug Typ-Informationen verfügbar, dann erlaubt der Compiler bei Lambda-Ausdrücken eine Abkürzung. Bei
Comparator<String> c = (String s1, String s2) -> { return s1.trim().compareTo( s2.trim() ); };
ist Typ-Inferenz einfach (Comparator<String> sagt alles aus), daher funktioniert die folgende Abkürzung:
Comparator<String> c = (s1, s2) -> { return s1.trim().compareTo( s2.trim() ); };
Die Parameterliste enthält also entweder explizit deklarierte Parametertypen oder implizite inferred-Typen. Eine Mischung ist nicht erlaubt, der Compiler blockt so etwas wie (String s1, s2) oder (s1, String s2) mit einem Fehler ab.
Wenn der Compiler die Typen ablesen kann, sind die Parametertypen optional. Aber Typ-Inferenz ist nicht immer möglich, weshalb die Abkürzung nicht immer möglich ist. Außerdem hilft die explizite Schreibweise auch der Lesbarkeit: kurze Ausdrücke sind nicht unbedingt die verständlichsten.
Hinweis
Der Compiler liest aus den Typen ab, ob alle Eigenschaften vorhanden sind. Die Typen sind dabei entweder explizit oder implizit gegeben.
Comparator<String> sc = (a, b) -> { return Integer.compare( a.length(), b.length() ); }; Comparator<BitSet> bc = (a, b) -> { return Integer.compare( a.length(), b.length() ); };
Die Klassen String und BitSet besitzen beide die Methode length(), daher ist der Lambda-Ausdruck korrekt. Der gleiche Lambda-Code lässt sich für zwei völlig verschiedene Klassen einsetzen, die überhaupt keine Gemeinsamkeiten haben, nur das sie zufällig beide eine Methode namens length() besitzen.
Abkürzung 2: Lambda-Rumpf ist entweder einzelner Ausdruck oder Block
Besteht der Rumpf eines Lambda-Ausdrucks nur aus einem einzelnen Ausdruck, kann eine verkürzte Schreibweise die Block-Klammern und das Semikolon einsparen. Statt
( LambdaParameter ) -> { return Ausdruck; }
heißt es dann
( LambdaParameter ) -> Ausdruck
Lambda-Ausdrücke mit einer return–Anweisung im Rumpf kommen häufig vor, da dies den typischen Funktionen entspricht. Somit ist es eine willkommene Verkürzung, wenn die abgekürzte Syntax für Lambda-Ausdrücke lediglich den Ausdruck fordert, der dann die Rückgabe bildet.
Hier sind drei Beispiele:
Lange Schreibweise | Abkürzung |
(s1, s2) ->
{ return s1.trim().compareTo( s2.trim() ); } |
(s1, s2) ->
s1.trim().compareTo( s2.trim() ) |
(a, b) -> { return a + b; } | (a, b) -> a + b |
() -> { System.out.println(); } | () -> System.out.println() |
Ausführliche und abgekürzte Schreibweise
Ausdrücke können in Java auch zu void ausgewertet werden, sodass ohne Probleme ein Aufruf wie System.out.println() in der kompakten Schreibweise ohne Block gesetzt werden kann. Das heißt, wenn Lambda-Ausdrücke mit der kurzen Ausdrucks-Syntax eingesetzt werden, können diese Ausdrücke etwas zurückgeben, müssen aber nicht.
Hinweis
Die Schreibweise mit den geschweiften Klammern und den Rückgabe-Ausdrücken kann nicht gemischt werden. Entweder gibt es ein Block geschweifter Klammern und return oder keine Klammern und kein return-Schlüsselwort. Fehler ergeben also diese falschen Mischungen:
Comparator<String> c; c = (s1, s2) -> { s1.trim().compareTo( s2.trim() ) }; // Compilerfehler (1) c = (s1, s2) -> return s1.trim().compareTo( s2.trim() ); // Compilerfehler (2)
Würden wir in (1) ein explizites return nutzen wäre alles in Ordnung, würde bei (2) das return wegfallen wäre die Zeile auch compilierbar.
Ob Lambda-Ausdrücke eine Rückgabe haben, drücken zwei Begriffe aus:
- void-kompatibel: Der Lambda-Rumpf gibt kein Ergebnis zurück. Entweder weil der Block kein return enthält, oder ein return ohne Rückgabe, oder weil ein void-Ausdruck in der verkürzten Schreibweise eingesetzt wird. Der Lambda-Ausdruck () -> System.out.println() ist also void-kompatibel, genauso wie () -> {}.
- Wert-kompatibel: Der Rumpf beendet den Lambda-Ausdruck mit einer return-Anweisung, die einen Wert zurückgibt oder besteht aus der kompakten Schreibenweise mit einer Rückgabe ungleich void.
Eine Mischung aus void- und Wert-kompatibel ist nicht erlaubt und führt wie bei Methoden zu einem Compilerfehler.[1]
Abkürzung 3: Einzelner Identifizierer statt Parameterliste und Klammern
Besteht die Parameterliste
a) nur aus einem einzelnen Identifizierer und
b) ist der Typ durch Typ-Inferenz klar,
können die runden Klammern wegfallen.
Lange Schreibweise | Typen inferred | Vollständig abgekürzt |
(String s) -> s.length() | (s) -> s.length() | s -> s.length() |
(int i) -> Math.abs( i ) | (i) -> Math.abs( i ) | i -> Math.abs( i ) |
Unterschiedlicher Grad von Abkürzungen
Kommen alle Abkürzungen zusammen, lässt sich etwa die Hälfte an Code einsparen. Aus (int i) -> { return Math.abs( i ); } wird einfach i -> Math.abs( i ).
Syntax-Hinweis
Nur bei genau einem Lambda-Parameter können die Klammern weggelassen werden, da es sonst Mehrdeutigkeiten gibt, für die es sonst wieder komplexe Regeln zur Auflösung geben müsste. Heißt es etwa foo( k, v -> { … } ) ist es unklar, ob foo zwei Parameter deklariert. Ist das zweite Argument ein Lambda-Ausdruck, oder handelt es sich um nur genau einen Parameter, wobei dann ein Lambda-Ausdruck übergeben wird, der selbst zwei Parameter deklariert. Um Probleme wie diesen aus dem Weg zu gehen, können Entwickler auf den ersten Blick sehen, dass foo( k, v -> { … } ) eindeutig für Parameter steht, und foo( (k, v) -> { … } ) nur einen Parameter besitzt.