1. Einführung
Seien wir ehrlich: Anonyme Klassen für ein oder zwei Zeilen Code zu schreiben – das ist, als würde man einen riesigen Lastwagen mieten, um ein einziges Brötchen von der Bäckerei in den Laden zu bringen.
Wenn Sie beispielsweise eine Liste von Strings nach ihrer Länge sortieren wollten, musste man vor Java 8 so schreiben:
List<String> list = Arrays.asList("Apfel", "Banane", "Kiwi");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Die Aufgabe ist eigentlich einfach, aber der Code füllt einen halben Bildschirm. Besonders nervig, wenn es davon viele gibt: Der Code wird „laut“, das Wesentliche geht verloren. Entwickler weinten und litten – und dann erfand man Lambda-Ausdrücke. Wir haben sie schon ein wenig kennengelernt; jetzt wiederholen wir und vertiefen unser Wissen.
Was ist ein Lambda-Ausdruck
Ein Lambda-Ausdruck ist eine kompakte Schreibweise für die Implementierung eines funktionalen Interfaces, also eines Interfaces mit genau einer abstrakten Methode (zum Beispiel Comparator, Runnable, Consumer und viele andere).
Kurz gesagt ermöglicht ein Lambda-Ausdruck, eine Funktion „on the fly“ genau dort zu schreiben, wo sie gebraucht wird, ohne eine eigene Klasse oder Methode zu deklarieren.
Allgemeine Syntax:
(Parameter) -> { Rumpf }
Beispiele:
- (a, b) -> a + b – eine Funktion zur Addition zweier Zahlen
- x -> x * x – eine Funktion, die eine Zahl quadriert
- () -> System.out.println("Hello!") – eine Funktion ohne Parameter
Bezug zu funktionalen Interfaces:
Einen Lambda-Ausdruck kann man stets einer Variablen vom Typ eines funktionalen Interfaces zuweisen oder als Argument an eine Methode übergeben, die ein solches Interface erwartet.
2. Syntax von Lambda-Ausdrücken
Ohne Parameter
Runnable r = () -> System.out.println("Hallo, Welt!");
r.run(); // Gibt aus: Hallo, Welt!
Ein Parameter
Wenn es genau einen Parameter gibt, können die Klammern weggelassen werden:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java ist cool!");
Mehrere Parameter
Klammern sind verpflichtend:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
Rumpf mit einem einzelnen Ausdruck
Besteht der Rumpf aus nur einem Ausdruck, sind geschweifte Klammern und return nicht nötig:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
Rumpf als Block
Wenn mehrere Anweisungen nötig sind, verwenden Sie geschweifte Klammern und return (falls es einen Rückgabewert gibt):
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
Parametertypen
Meist müssen Parametertypen nicht angegeben werden – der Compiler leitet sie aus dem Kontext ab. Man kann sie aber auch explizit angeben:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
Lambda ohne Rückgabewert
Wenn das Interface void zurückgibt, schreiben Sie einfach die Anweisungen:
list.forEach(s -> System.out.println("Element: " + s));
Tabelle: Varianten der Syntax von Lambda-Ausdrücken
| Was tun wir | Beispiel | Kommentar |
|---|---|---|
| Ohne Parameter | |
Zum Beispiel für Runnable |
| Ein Parameter | |
Ohne Klammern möglich |
| Mehrere Parameter | |
Klammern sind Pflicht |
| Ein einzelner Ausdruck | |
Ohne return und ohne geschweifte Klammern |
| Codeblock | |
Mit return, falls es ein Ergebnis gibt |
3. Anwendung: wo und wie man Lambda-Ausdrücke verwendet
Lambda-Ausdrücke werden vor allem dort eingesetzt, wo „Verhalten“ – also eine Funktion – als Argument übergeben werden soll. Das war eine echte Revolution für Collections, Streams (Stream API), Events und vieles mehr.
Sortieren einer Liste
Vor Java 8:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Mit Lambda-Ausdruck:
list.sort((a, b) -> a.length() - b.length());
Erzeugen eines Threads
Thread t = new Thread(() -> System.out.println("Thread gestartet!"));
t.start();
Bearbeiten von Collections
list.forEach(s -> System.out.println(s.toUpperCase()));
Filtern einer Liste
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
Beispiel: Wir entwickeln unsere Lernanwendung weiter
Angenommen, wir haben eine Liste von Benutzern:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
Alle Benutzer ausgeben, deren Namen länger als 4 Zeichen sind:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("Benutzer: " + name));
4. Geltungsbereich von Variablen in Lambda-Ausdrücken
Lambda-Ausdrücke können Variablen aus der umgebenden Methode verwenden, aber es gibt Feinheiten!
Variablen und Lambdas
Eine Lambda in Java darf nur solche Variablen „capturen“, die sich nach ihrer Initialisierung nicht mehr ändern. Wenn die Variable mit final deklariert ist, ist es offensichtlich. Aber selbst wenn das Wort final nicht dasteht, prüft der Compiler, ob sich der Wert ändert oder nicht. Wenn nicht, betrachtet er sie als „effektiv final“ und lässt sie in der Lambda zu.
Beispiel:
int minLength = 4; // Der Wert ändert sich nirgends
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
Das funktioniert, weil minLength derselbe Wert bleibt.
Wenn Sie jedoch nach der Verwendung in der Lambda versuchen, minLength neu zuzuweisen, erhalten Sie einen Compilerfehler:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // Fehler! Die Lambda hat den Wert bereits "eingefroren"
Im Grunde ist die Regel sehr einfach: Eine Variable, die in eine Lambda gelangt, muss unveränderlich sein.
Unterschied zu anonymen Klassen
In anonymen Klassen und Lambda-Ausdrücken gelten für Variablen aus der äußeren Methode dieselben Regeln: nur final/effektiv final.
ABER!
Bei Lambda-Ausdrücken bezieht sich this auf das äußere Objekt (die aktuelle Instanz der Klasse), bei einer anonymen Klasse auf die Instanz der anonymen Klasse. Das ist wichtig, wenn Sie innerhalb der Lambda auf Felder oder Methoden der aktuellen Klasse zugreifen.
Beispiel:
public class Example {
String name = "Äußere Klasse";
void demo() {
Runnable r1 = new Runnable() {
String name = "Anonyme Klasse";
@Override
public void run() {
System.out.println(this.name); // "Anonyme Klasse"
}
};
Runnable r2 = () -> System.out.println(this.name); // "Äußere Klasse"
r1.run();
r2.run();
}
}
5. Typische Fehler im Umgang mit Lambda-Ausdrücken
Fehler Nr. 1: Verwendung einer nicht final/nicht effektiv finalen Variable. Wenn Sie eine Variable ändern, nachdem Sie sie in einer Lambda verwendet haben, weist Sie der Compiler sofort zurecht. Das dient der Sicherheit: Andernfalls wäre unklar, welchen Wert der Variable man verwenden soll.
Fehler Nr. 2: Verwechslung mit this. In einem Lambda-Ausdruck ist this die äußere Klasse, in einer anonymen Klasse die anonyme Klasse selbst. Wenn Sie versuchen, eine Methode der äußeren Klasse aus einer Lambda aufzurufen, funktioniert das; aus einer anonymen Klasse hingegen nicht (falls Sie auf den Kontext der äußeren Klasse hoffen).
Fehler Nr. 3: Lambda ohne Kontext. Ein Lambda-Ausdruck kann nicht für sich allein stehen – er muss entweder einer Variable eines funktionalen Interfaces zugewiesen oder dorthin übergeben werden, wo dieses Interface erwartet wird. Der Versuch, einfach x -> x + 1 außerhalb eines Kontexts zu schreiben, führt zu einem Fehler.
Fehler Nr. 4: Zu komplexe Lambda. Wenn ein Lambda-Ausdruck länger als 3–5 Zeilen wird, ist er schwer zu lesen. In solchen Fällen sollten Sie die Logik in eine eigene Methode auslagern.
GO TO FULL VERSION