1. Einführung in Closures
Ein Closure ist eine Funktion (oder Funktionsobjekt), die nicht nur ihre Parameter verwendet, sondern sich auch Variablen aus dem umgebenden Kontext „merkt“, in dem sie erstellt wurde. Vereinfacht gesagt: Wenn ein Lambda-Ausdruck oder eine anonyme Klasse innerhalb einer Methode Variablen aus dieser Methode verwendet – dann ist es ein Closure.
Ein einfaches Beispiel
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "Hallo, ";
Runnable sayHello = () -> System.out.println(greeting + "Welt!");
sayHello.run(); // Gibt aus: Hallo, Welt!
}
}
Hier hat die Lambda die Variable greeting aus der äußeren Methode „erfasst“ und verwendet sie in sich selbst. Das ist ein Closure!
2. Effektiv finale Variablen: Was ist das und wozu?
In Java dürfen Lambda-Ausdrücke (und anonyme Klassen) nur solche Variablen aus der äußeren Methode verwenden, die als final deklariert sind oder nach der Initialisierung nicht mehr verändert werden. Solche Variablen nennt man effektiv final.
Warum ist das so?
Diese Einschränkung hängt damit zusammen, dass ein Lambda-Ausdruck auch nach dem Ende der Methode aufgerufen werden kann, in der er erstellt wurde. Könnte die Variable verändert werden, entstünde Verwirrung: Welche Version der Variable soll verwendet werden? Um Überraschungen zu vermeiden, verlangt Java, dass die Variable unveränderlich ist (oder zumindest wie unveränderlich erscheint).
Beispiel: korrekte Verwendung
public static void main(String[] args) {
int number = 42; // number – effektiv final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
Beispiel: Versuch, die Variable nach der Verwendung zu ändern
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // FEHLER: Die Variable number muss final oder effektiv final sein
r.run();
}
Der Compiler meldet einen Fehler: Variable used in lambda expression should be final or effectively final.
Effektiv final – das ist ... eine Variable, der genau einmal ein Wert zugewiesen wird und die danach nicht mehr verändert wird. final muss man nicht unbedingt schreiben, der Compiler erkennt das selbst.
3. Wie erfassen Lambda-Ausdrücke Variablen?
Wenn Sie eine Lambda schreiben, die eine externe Variable verwendet, „packt“ Java diese Variable zusammen mit der Lambda. Selbst wenn die Methode, in der die Lambda erstellt wurde, bereits beendet ist, verschwindet die Variable nicht – sie lebt innerhalb des Closures weiter.
Illustration: Die Lambda „merkt sich“ die Variable
public static Runnable createGreeter(String name) {
// name – Parameter der Methode, wird von der Lambda erfasst
return () -> System.out.println("Hallo, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Rutger");
greeter.run(); // Hallo, Rutger!
}
Hier existiert die Variable name im Stack der Methode main nicht mehr, aber greeter „merkt“ sich weiterhin ihren Wert.
Wie ist das intern implementiert?
Der Java-Compiler erstellt ein spezielles Hilfsobjekt (auch „Capture/Display-Klasse“ genannt), das alle erfassten Variablen speichert. Der Lambda-Ausdruck wird zu einem Objekt, das eine Referenz auf diesen „Container“ mit den Variablen hat.
4. Beispiel eines Closures: eine Funktion zurückgeben, die eine Variable verwendet
Wir schreiben eine Funktion, die eine Lambda zurückgibt, die eine Variable aus ihrem Kontext verwendet:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x – wird von der Lambda erfasst
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
Hier ist die Variable x bereits aus dem Stack der Methode „verschwunden“, aber die Lambda kann sie weiterhin verwenden.
5. Warum darf man erfasste Variablen nicht ändern?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // FEHLER!
printer.run();
}
Der Compiler lässt das nicht zu. Wenn wir base ändern könnten, wäre unklar, welche Version der Variable in der Lambda verwendet werden soll: die alte oder die neue? Deshalb verbietet Java das Ändern lokaler Variablen, die von einer Lambda erfasst wurden.
Was kann man in einer Lambda verwenden?
- Lokale Variablen, die nach der Initialisierung nicht mehr verändert werden (effektiv final).
- Felder einer Klasse (sowohl static als auch nicht statisch) – diese dürfen geändert werden, aber das ist ein anderer Mechanismus (Zugriff auf den Objektzustand) und kein Erfassen einer lokalen Variable.
6. Vergleich mit anonymen Klassen
Bevor es Lambda-Ausdrücke in Java gab, konnte man Closures mit anonymen Klassen bauen:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
Die Regeln sind dieselben: Die Variable word muss final oder effektiv final sein.
Unterschied: Gültigkeitsbereich von this
- In einer anonymen Klasse verweist this auf die Instanz der anonymen Klasse.
- In einem Lambda-Ausdruck verweist this auf das äußere Objekt (z. B. die aktuelle Instanz der Klasse).
7. Closures und Klassenfelder
Wenn eine Lambda ein Klassenfeld verwendet, ist das im strengen Sinn kein „Erfassen einer lokalen Variable“ – das Feld ist immer verfügbar und kann geändert werden.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("Zähler: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // Zähler: 1
r.run(); // Zähler: 2
}
}
8. Typische Fehler und Besonderheiten von Closures in Java
Fehler Nr. 1: Versuch, die von einer Lambda erfasste Variable zu ändern. Der häufigste Fehler ist der Versuch, eine lokale Variable zu verändern, nachdem sie in einer Lambda verwendet wurde. Der Compiler meldet: Variable used in lambda expression should be final or effectively final.
Fehler Nr. 2: Erwartung, dass die Variable „eingefroren“ ist. In Java ist die erfasste Variable, wenn es um ein Klassenfeld geht, keine Kopie des Werts, sondern eine Referenz auf das Original. Wenn sich ein Klassenfeld ändert, sieht die Lambda den neuen Wert. Lokale Variablen in Lambdas müssen jedoch effektiv final sein.
Fehler Nr. 3: Erwartung, dass eine Lambda einen neuen Gültigkeitsbereich für this erzeugt. In einer Lambda ist this das äußere Objekt (die umschließende Klasse). In einer anonymen Klasse ist this die anonyme Klasse selbst.
Fehler Nr. 4: Verwendung veränderlicher Objekte. Wenn Sie eine Referenz auf ein veränderliches Objekt (z. B. eine Liste) erfassen, können Sie dessen Inhalt innerhalb der Lambda ändern, auch wenn die Variable selbst effektiv final ist:
public static void main(String[] args) {
java.util.List<String> list = new java.util.ArrayList<>();
Runnable r = () -> list.add("Hello");
r.run();
System.out.println(list); // [Hello]
}
Hier ändert sich die Variable list nicht (wir machen kein list = ...), aber das Objekt, auf das sie zeigt, wird verändert.
GO TO FULL VERSION