Verhalten, das du herumreichen kannst
Ein Lambda-Ausdruck ist ein kompakter Codeblock, den du an eine Methode übergeben, in einer Variablen speichern oder zurückgeben kannst - genau wie eine Zahl oder eine Zeichenkette. Vor den Lambdas bedeutete das Weitergeben von Verhalten, eine ganze Klasse (oder eine umständliche anonyme Klasse) zu schreiben, nur um eine einzige Methode zu verpacken. Ein Lambda reduziert das auf das Wesentliche: die Parameter und den Rumpf.
Ein Lambda implementiert immer ein funktionales Interface - ein Interface mit genau einer abstrakten Methode (du bist ihnen am Ende der Seite über Interfaces begegnet). Der Compiler erschließt aus dem Kontext, welches Interface du meinst, und ordnet dein Lambda dieser einen Methode zu.
x -> x * 2 ist die vollständige Implementierung von apply. Kein new, kein Klassenrumpf, kein Methodenname - das alles liefert das Interface.
Die Pfeil-Syntax
Jedes Lambda hat die Form Parameter -> Rumpf. Die Bestandteile passen sich an, je nachdem, wie viel du brauchst:
() -> 42 // keine Parameter
x -> x + 1 // ein Parameter, Klammern optional
(x, y) -> x + y // zwei oder mehr Parameter brauchen Klammern
(int x, int y) -> x + y // Typen sind optional - meist abgeleitet
x -> { // ein Block-Rumpf braucht geschweifte Klammern und return
int doubled = x * 2;
return doubled + 1;
}
Ein Rumpf mit einem einzigen Ausdruck (x -> x + 1) gibt seinen Wert implizit zurück - ohne das Schlüsselwort return. Sobald du geschweifte Klammern verwendest, schreibst du einen normalen Block und musst return explizit angeben, wenn die Methode etwas zurückgibt. Ein häufiger Fehler ist, beides zu vermischen: x -> { x + 1 } kompiliert nicht, weil ein Block eine Anweisung benötigt (return x + 1;).
Lambdas ersetzen anonyme Klassen
Am deutlichsten zeigt sich der Nutzen eines Lambdas im Vorher/Nachher. Das Sortieren mit einem eigenen Comparator sah früher so aus:
// vorher - anonyme Klasse
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Dasselbe als Lambda ist eine einzige gut lesbare Zeile:
Comparator ist ein funktionales Interface (seine einzige abstrakte Methode ist compare), daher fügt sich das Lambda direkt ein. Das ganze Drumherum - das new, die Klasse, die Methodensignatur - fällt weg, und es bleibt nur die Vergleichslogik.
Der Werkzeugkasten java.util.function
Nur selten musst du ein eigenes funktionales Interface deklarieren. Das Paket java.util.function bringt die gängigen Formen mit, und nahezu alle Bibliotheks-APIs von Java akzeptieren sie:
Function<T, R>- nimmt einTentgegen, gibt einRzurück (apply)Predicate<T>- nimmt einTentgegen, gibt einbooleanzurück (test)Supplier<T>- nimmt nichts entgegen, gibt einTzurück (get)Consumer<T>- nimmt einTentgegen, gibt nichts zurück (accept)
Das sind generische Interfaces - Function<String, Integer> verwendet die Generics wieder, die du auf der vorherigen Seite gesehen hast, um typsicher zu bleiben. Wähle dasjenige, dessen Form zu dem passt, was dein Code entgegennehmen und erzeugen muss.
Methodenreferenzen
Wenn ein Lambda nichts weiter tut, als eine bereits vorhandene Methode aufzurufen, kannst du es mit :: durch eine Methodenreferenz ersetzen. Es ist derselbe Wert, nur direkter geschrieben:
Methodenreferenzen gibt es in verschiedenen Varianten: String::toUpperCase (eine Instanzmethode, die auf jedem Argument aufgerufen wird), Math::max (eine statische Methode), System.out::println (eine Methode auf einem bestimmten Objekt) und ArrayList::new (ein Konstruktor). Greife nur dann zu einer, wenn sie sich klar liest - musst du erst überlegen, welche Form zutrifft, ist ein schlichtes Lambda völlig in Ordnung.
Erfassen von Variablen
Ein Lambda kann lokale Variablen aus der umgebenden Methode verwenden, aber nur, wenn sie final oder effektiv final sind - einmal zugewiesen und nie verändert. Java erfasst den Wert in dem Moment, in dem das Lambda erzeugt wird, daher wäre eine Variable, die sich später ändern könnte, mehrdeutig.
Wenn du factor irgendwo neu zuweist, lehnt der Compiler das Lambda mit "variable used in lambda expression should be final or effectively final" ab. Wenn du wirklich veränderlichen gemeinsamen Zustand brauchst, erfasse stattdessen ein Objekt - ein Feld, ein Array-Element oder einen AtomicInteger -, denn die Referenz bleibt fest, auch wenn sich ihr Inhalt ändert. Beachte, dass Lambdas, anders als anonyme Klassen, keinen eigenen Gültigkeitsbereich erzeugen: this innerhalb eines Lambdas bezieht sich auf die umgebende Instanz, nicht auf das Lambda selbst.
Als Nächstes: Streams
Lambdas sind der Baustein, nicht das Ziel. Ihr eigentlicher Nutzen zeigt sich mit der Streams-API, in der du Operationen wie filter, map und reduce verkettest - jede nimmt ein Lambda entgegen -, um Datentransformationen als gut lesbare Pipeline auszudrücken statt als Gewirr von Schleifen. Das ist die nächste Seite.
Häufig gestellte Fragen
Was ist ein Lambda-Ausdruck in Java?
Ein Lambda ist eine kurze Möglichkeit, eine Instanz eines funktionalen Interface zu schreiben - also eines Interface mit genau einer abstrakten Methode. Statt einer ganzen anonymen Klasse schreibst du Parameter -> Rumpf. Der Compiler ordnet das Lambda der einen Methode des Interface zu, sodass Runnable r = () -> System.out.println("hi"); ein vollständiges Runnable ist. Mit Lambdas kannst du Verhalten wie Daten herumreichen.
Was ist der Unterschied zwischen einem Lambda und einer Methodenreferenz in Java?
Beide erzeugen eine Instanz eines funktionalen Interface. Ein Lambda hat explizite Parameter und einen Rumpf (s -> s.toUpperCase()), während eine Methodenreferenz die Kurzform eines Lambdas ist, das lediglich eine bereits vorhandene Methode aufruft (String::toUpperCase). Verwende eine Methodenreferenz, wenn das Lambda nichts weiter tun würde, als seine Argumente an eine einzelne benannte Methode weiterzureichen - das ist kürzer und liest sich besser.
Warum müssen in einem Java-Lambda verwendete Variablen final oder effektiv final sein?
Ein Lambda kann lokale Variablen aus der umgebenden Methode erfassen, aber nur, wenn sie sich nach der Zuweisung nie ändern - genau das bedeutet "effektiv final". Java erfasst den Wert, nicht eine lebende Referenz auf die Variable, daher wäre es mehrdeutig und über Threads hinweg unsicher, eine erneute Zuweisung zu erlauben. Wenn du veränderlichen gemeinsamen Zustand brauchst, verwende stattdessen ein Feld oder einen Wrapper wie ein Array oder einen AtomicInteger.