Geschichte der Frage:
Bis Java 8 mussten Variablen aus dem äußeren Sichtbereich, die in inneren Klassen oder anonymen Klassen verwendet wurden, als final deklariert werden. In Java 8 wurde diese Anforderung gelockert: Jetzt kann eine Variable nicht als final deklariert werden, wenn sie faktisch nicht geändert wird (effectively final).
Problem:
Lambda-Ausdrücke und innere Klassen verwenden Closures über Variablen aus dem äußeren Block. Wenn diese Variablen jedoch ihren Wert ändern, entsteht Verwirrung und unerwartetes Verhalten — es ist unklar, welchen Wert man verwenden soll.
Lösung:
Der Compiler erlaubt die Verwendung einer Variablen in einer inneren Klasse oder Lambda nur, wenn sie effectively final ist — das heißt, sie wurde nach der Initialisierung nicht mehr geändert, auch wenn sie nicht explizit als final deklariert ist.
Beispielcode:
public void demo() { int x = 10; Runnable r = () -> System.out.println(x); // x ist effectively final r.run(); }
Wenn man versucht, x zu ändern:
public void demo() { int x = 10; x = 20; // jetzt ist x nicht mehr effectively final Runnable r = () -> System.out.println(x); // Compilerfehler }
Wichtige Merkmale:
Kann man veränderbare Objekte verwenden, wenn der Verweis effectively final ist?
Ja, wenn der Verweis nicht geändert wird, aber das Objekt über diesen Verweis geändert wird — das ist erlaubt. Zum Beispiel,
List<String> list = new ArrayList<>(); list.add("A"); Runnable r = () -> System.out.println(list.get(0)); // OK list = new ArrayList<>(); // Wenn so, wird es einen Compilerfehler geben
Kann man eine Variable als final deklarieren und dann trotzdem den Inhalt des Objekts ändern?
Ja. final bezieht sich auf den Verweis, nicht auf den Inhalt des Objekts. Den Zustand des Objekts über den Verweis zu ändern, ist zulässig. Zum Beispiel,
final List<Integer> nums = new ArrayList<>(); nums.add(5); // OK nums = new ArrayList<>(); // Fehler
Kann man Parameter in Lambdas verwenden?
Ja, wenn sie ebenfalls effectively final sind — das heißt, sie werden im Methodenkörper nach der Initialisierung nicht verändert.
Im Methodenkörper werden Variablen verwendet, die zufällig nach der Erstellung der Lambda überschrieben werden, wodurch das Programm nicht kompiliert und Zeit für die Ursachenforschung verloren geht.
Vorteile:
Nachteile:
Der Entwickler verwendet einen inkrementierbaren Wert über AtomicInteger (oder ein anderes Holder-Objekt), das einen Verweis speichert, nicht den Wert, und gewährleistet damit das korrekte Funktionieren von Lambdas, auch wenn der Zähler innerhalb des Lambda-Ausdrucks geändert werden muss.
Vorteile:
Nachteile: