Historia pytania:
Przed Javą 8, aby używać zmiennych z zewnętrznej przestrzeni widzenia w klasach wewnętrznych lub anonimowych, te zmienne musiały być deklarowane jako final. W Javie 8 wymóg ten został złagodzony: teraz zmienna może nie być oznaczona jako final, jeśli de facto nie jest zmieniana (effectively final).
Problem:
Wyrażenia lambda i klasy wewnętrzne używają zamknięć nad zmiennymi z zewnętrznego bloku. Jednak jeśli takie zmienne zmieniają wartość, powstaje zamieszanie i nieprawidłowe zachowanie — trudno jest zrozumieć, którą wartość użyć.
Rozwiązanie:
Kompilator pozwala używać zmiennej w klasie wewnętrznej lub lambdzie tylko wtedy, gdy jest ona effectively final — to znaczy ani razu nie była zmieniana po inicjalizacji, nawet jeśli nie jest jawnie zadeklarowana jako final.
Przykład kodu:
public void demo() { int x = 10; Runnable r = () -> System.out.println(x); // x — effectively final r.run(); }
Jeśli spróbujesz zmienić x:
public void demo() { int x = 10; x = 20; // teraz x nie jest effectively final Runnable r = () -> System.out.println(x); // Błąd kompilacji }
Kluczowe cechy:
Czy można używać zmiennych obiektów, jeśli sam odnośnik jest effectively final?
Tak, jeśli odnośnik nie zmienia się, ale obiekt pod tym odnośnikiem się zmienia — to jest dozwolone. Na przykład,
List<String> list = new ArrayList<>(); list.add("A"); Runnable r = () -> System.out.println(list.get(0)); // OK list = new ArrayList<>(); // Jeśli tak, będzie błąd kompilacji
Czy można zadeklarować zmienną jako final, a następnie nadal zmieniać zawartość obiektu?
Tak. final odnosi się do odnośnika, a nie do zawartości obiektu. Zmiana stanu obiektu przez odnośnik — jest dozwolona. Na przykład,
final List<Integer> nums = new ArrayList<>(); nums.add(5); // OK nums = new ArrayList<>(); // Błąd
Czy można używać zmiennych argumentów metody w lambdach?
Tak, jeśli są również effectively final — to znaczy nie zmieniają się w metodzie po inicjalizacji.
W metodzie używane są zmienne, które przypadkowo są nadpisywane po utworzeniu lambdy, w wyniku czego program nie kompiluje się, a czas marnowany jest na ustalanie przyczyny.
Zalety:
Wady:
Programista używa inkrementowanej wartości przez AtomicInteger (lub inny obiekt holder), który przechowuje odnośnik, a nie wartość, zapewniając poprawne działanie lambd, nawet jeśli wymagane jest modyfikowanie licznika w wyrażeniu lambda.
Zalety:
Wady: