W3docs

Java Lambda-Ausdrücke

Kompakte Inline-Implementierungen von funktionalen Interfaces in Java mit Lambda-Ausdrücken: (params) -> body.

Ein Lambda-Ausdruck ist die kompakte Syntax, die Java 8 für „eine Instanz eines Interface mit genau einer abstrakten Methode" eingeführt hat. Früher wurde das als anonyme Klasse geschrieben. Danach schreibt man es als Parameterliste, einen Pfeil und einen Rumpf:

Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();

Es gibt keinen neuen Werttypr, byLen und length sind nach wie vor Objektreferenzen, und zur Laufzeit enthält jede eine Instanz einer Klasse, die das Interface auf der linken Seite implementiert. Neu ist, dass der Code, der sagt „erstelle mir eine solche Instanz", kurz genug ist, um direkt an der Aufrufstelle zu stehen — und das ermöglicht alle anderen funktionalen Idiome in diesem Teil: Filter-Prädikate, Comparator-Builder, Event-Handler, Stream-Pipelines.

Die Syntaxformen

Ein Lambda besteht aus drei Teilen: Parameterliste, Pfeil -> und Rumpf. Jeder Teil hat eine Kurzform:

// Zero parameters: empty parens are required
Runnable r = () -> System.out.println("tick");

// One parameter: parens optional (idiomatic to omit them)
Function<String, Integer> len = s -> s.length();
Function<String, Integer> len2 = (s) -> s.length();           // same thing

// Two or more: parens required
Comparator<String> cmp = (a, b) -> a.length() - b.length();

// Explicit types: rare but legal
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;

// Expression body: the value of the expression is the return value
Predicate<Integer> positive = n -> n > 0;

// Block body: explicit `return` required if the interface method returns a value
Function<Integer, String> describe = n -> {
  if (n == 0) return "zero";
  if (n < 0)  return "negative";
  return "positive";
};

Drei Regeln verbinden diese Formen:

  1. Parametertypen werden in der Regel inferiert aus dem Zieltyp (dem an der Aufrufstelle deklarierten Interface). Schreibe sie nur, wenn der Compiler keinen ableiten kann oder wenn sie die Lesbarkeit verbessern.
  2. Ein Ausdrucks-Rumpf gibt seinen Wert implizit zurück. Kein return, kein Semikolon. Der Ausdruck ist das Ergebnis.
  3. Ein Block-Rumpf benötigt return, wenn die Methode des Interface einen Rückgabetyp hat. Das Vergessen ist ein Kompilierfehler, kein stilles null.

Zieltypisierung — wo Lambdas auftreten können

Ein Lambda hat keinen intrinsischen Typ. Der Compiler bestimmt seinen Typ aus dem Ziel — dem Kontext, in dem es verwendet wird:

Runnable           r1 = () -> doWork();          // target: Runnable
Callable<Integer>  c1 = () -> 42;                // target: Callable<Integer>
Supplier<Integer>  s1 = () -> 42;                // target: Supplier<Integer>

() -> 42 ist in allen drei Fällen dieselbe Quelle, wird aber zu drei verschiedenen Interface-Instanzen kompiliert. Deshalb kann ein Lambda nicht direkt Object zugewiesen werden — Object o = () -> 42; ist mehrdeutig und der Compiler verweigert dies. Mit einem Cast lässt sich die Mehrdeutigkeit auflösen: Object o = (Supplier<Integer>) () -> 42;.

Die häufigsten Ziele:

  • Ein Methodenparameter, der als funktionales Interface typisiert ist: list.removeIf(s -> s.isEmpty()).
  • Ein Feld oder eine lokale Variable eines funktionalen Interface-Typs: Predicate<String> empty = String::isEmpty;.
  • Ein Rückgabetyp: public Supplier<Date> now() { return Date::new; }.

Gibt es kein Ziel, gibt es kein Lambda. var f = s -> s.length(); kompiliert nichtvar kann keinen Zieltyp inferieren.

Variablenerfassung: „effektiv final"

Ein Lambda kann lokale Variablen aus der umgebenden Methode lesen, aber nur wenn diese Variablen effektiv final sind — d. h. nach ihrer Initialisierung nie neu zugewiesen werden:

int multiplier = 3;
IntFunction<Integer> scale = n -> n * multiplier;     // OK — `multiplier` never reassigned
multiplier = 4;                                        // <-- this line would make the lambda not compile

Die Regel ist dieselbe, die anonyme innere Klassen schon immer hatten, und der Grund ist derselbe: Ein Lambda kann die Methode überleben, in der es definiert wurde (man könnte es in einem Feld speichern oder an einen anderen Thread übergeben), und Java hat keine Closures, die die Variable erfassen — es erfasst den Wert zum Zeitpunkt der Konstruktion. Neuzuweisungen zu erlauben würde eine verwirrende Illusion erzeugen.

Felder sind eine andere Geschichte. Ein Lambda kann Instanz- und statische Felder frei lesen und verändern:

class Counter {
  private int n = 0;
  Runnable inc = () -> n++;     // legal — `n` is a field, not a local
}

Dies ist eine häufige Fehlerquelle in Stream-Code — ein Lambda, das ein gemeinsames Feld verändert, wirkt harmlos, hat aber Race Conditions mit sich selbst, wenn der Stream parallel läuft. Reine Lambdas sind sicherer.

this, return und break innerhalb eines Lambdas

Ein Lambda ist kein neuer Gültigkeitsbereich für this. Innerhalb eines Lambdas bezieht sich this auf die umgebende Instanz — genauso wie der umgebende Code:

class Greeter {
  String prefix = "Hello, ";
  Function<String, String> greet = name -> this.prefix + name;   // `this` is the Greeter
}

Das ist einer der wichtigsten praktischen Unterschiede zu anonymen Klassen, bei denen sich this auf die anonyme Instanz selbst bezog.

return innerhalb eines Lambdas gibt aus dem Lambda zurück, nicht aus der umgebenden Methode. break und continue funktionieren in einem Lambda nicht — sie gehören zur Schleife, auf die sie abzielen, und der Lambda-Rumpf ist kein Teil der umgebenden Schleife.

Lambda vs. anonyme Klasse — wann welche passt

Für funktionale Interfaces sind Lambdas fast immer kürzer und klarer. Sie erzeugen leicht unterschiedlichen Bytecode (invokedynamic) und erstellen keine neue Klassendatei pro Verwendungsstelle, sodass sie zur Laufzeit in der Regel leichtgewichtiger sind.

Verwende eine anonyme Klasse, wenn:

  • Das Interface mehr als eine abstrakte Methode hat (es ist nicht funktional).
  • Du ein methodenlokales Feld benötigst (int seen = 0;, das über Aufrufe hinweg zugänglich ist).
  • Du this brauchst, um auf die erstellte Instanz zu verweisen, nicht auf die umgebende Instanz.
  • Du eine Default-Methode überschreiben musst, um ihr Verhalten zu spezialisieren.

In jedem anderen Fall gewinnt das Lambda.

Ein ausgearbeitetes Beispiel: Erfassung, Zieltypisierung, die vier Aufrufstellen

Das folgende Programm demonstriert die vier häufigsten Stellen, an denen ein Lambda erscheint — Collection forEach, removeIf, Sortierung und Stream-Filter — zusammen mit den Erfassungsregeln und der Zieltypisierung.

java— editable, runs on the server

Was man aus der Ausführung mitnehmen sollte:

  • () -> \"hi\" funktionierte sowohl als Callable<String> als auch als Supplier<String> — gleiche Quelle, unterschiedliche Zieltypen, unterschiedliche Interface-Instanzen. Deshalb hat ein Lambda keinen Typ, bis der Kontext einen bereitstellt.
  • times = n -> n * factor erfasste factor nach Wert. Der Compiler akzeptierte es, weil factor nie neu zugewiesen wurde. Das Einkommentieren von factor = 11 würde factor zu einer nicht-effektiv-finalen Variable machen und die Kompilierung des Lambdas brechen.
  • forEach, removeIf und sort nehmen alle ein unterschiedliches funktionales Interface (Consumer, Predicate, Comparator), und die Form des Lambdas — Anzahl der Parameter, Vorhandensein eines Rückgabewerts — passte zur einzelnen abstrakten Methode jedes Interface. Der Compiler führt den Abgleich durch Zieltypisierung durch.
  • Das Block-Rumpf-Lambda describe benötigte explizite return-Anweisungen, weil sein Ziel (Function<Integer, String>) einen Nicht-void-Rückgabetyp hat. Die Ausdrucks-Rumpf-Lambdas darüber gaben ihren Ausdruck implizit zurück.

Was als nächstes kommt

Du kennst die Syntax und die Erfassungsregeln. Die nächste Frage lautet: Welches Interface genau kompiliert ein Lambda zu? Java Functional Interfaces stellt die Single-Abstract-Method(SAM)-Regel, die @FunctionalInterface-Annotation und die Erstellung eigener funktionaler Interfaces für Fälle vor, die die Standardbibliothek nicht abdeckt.

Übungen

Übung
Welchen dieser Lambda-Ausdrücke wird der Java-Compiler ablehnen?
Welchen dieser Lambda-Ausdrücke wird der Java-Compiler ablehnen?
Was this page helpful?