1. Wprowadzenie do domknięć
Domknięcie — to funkcja (albo obiekt-funkcja), która nie tylko używa swoich parametrów, lecz także „zapamiętuje” zmienne z otaczającego kontekstu, w którym została utworzona. Mówiąc prościej: jeśli wyrażenie lambda lub klasa anonimowa wewnątrz metody wykorzystuje zmienne z tej metody — staje się domknięciem.
Przykład „na palcach”
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "Cześć, ";
Runnable sayHello = () -> System.out.println(greeting + "świecie!");
sayHello.run(); // Wypisze: Cześć, świecie!
}
}
Tutaj lambda „przechwyciła” zmienną greeting z metody zewnętrznej i używa jej w swoim wnętrzu. To właśnie jest domknięcie!
2. Efektywnie finalne zmienne: co to jest i po co?
W Javie wyrażenia lambda (i klasy anonimowe) mogą używać tylko takich zmiennych z metody zewnętrznej, które są zadeklarowane jako final lub nie są zmieniane po inicjalizacji. Takie zmienne nazywa się efektywnie finalnymi.
Dlaczego tak jest?
To ograniczenie wynika z tego, że wyrażenie lambda może zostać wywołane po zakończeniu działania metody, w której zostało utworzone. Gdyby zmienna mogła się zmieniać, pojawiłby się problem: której wersji zmiennej użyć? Aby uniknąć niespodzianek, Java wymaga, by zmienna była niemodyfikowalna (albo przynajmniej wyglądała na niemodyfikowalną).
Przykład: poprawne użycie
public static void main(String[] args) {
int number = 42; // number — efektywnie finalna
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
Przykład: próba zmiany zmiennej po użyciu
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // BŁĄD: zmienna number musi być final albo efektywnie finalna
r.run();
}
Kompilator wyświetli błąd: Variable used in lambda expression should be final or effectively final.
Efektywnie finalna — to... zmienna, której wartość jest przypisana dokładnie raz i dalej się nie zmienia. Nie trzeba pisać final, kompilator sam to rozpozna.
3. Jak wyrażenia lambda przechwytują zmienne?
Gdy piszesz lambdę, która używa zmiennej zewnętrznej, Java „pakuje” tę zmienną razem z lambdą. Nawet jeśli metoda, w której utworzono lambdę, już się zakończyła, zmienna nie znika — żyje wewnątrz domknięcia.
Ilustracja: lambda „zapamiętuje” zmienną
public static Runnable createGreeter(String name) {
// name — parametr metody, zostanie przechwycony przez lambdę
return () -> System.out.println("Cześć, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Vasya");
greeter.run(); // Cześć, Vasya!
}
Tutaj zmienna name nie istnieje już na stosie metody main, ale greeter wciąż „pamięta” jej wartość.
Jak to jest zrealizowane wewnętrznie?
Kompilator Javy tworzy specjalny obiekt pomocniczy (nazywany też „capture/display class”), który przechowuje wszystkie przechwycone zmienne. Wyrażenie lambda staje się obiektem, który ma referencję do tego „kontenera” ze zmiennymi.
4. Przykład domknięcia: zwracamy funkcję, używającą zmiennej
Napiszmy funkcję, która zwraca lambdę, używającą zmiennej ze swojego kontekstu:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x — jest przechwytywana przez lambdę
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
Tutaj zmienna x „zeszła” już ze stosu metody, ale lambda wciąż może jej używać.
5. Dlaczego nie wolno zmieniać przechwyconych zmiennych?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // BŁĄD!
printer.run();
}
Kompilator na to nie pozwoli. Gdybyśmy mogli zmieniać base, nie byłoby jasne, której wersji zmiennej użyć w lambdzie: starej czy nowej? Dlatego Java zabrania zmieniać lokalne zmienne przechwycone przez lambdę.
Czego można używać w lambdzie?
- Zmienne lokalne, które nie zmieniają się po inicjalizacji (efektywnie finalne).
- Pola klasy (zarówno static, jak i niestatyczne) — można je zmieniać, ale to już inny mechanizm (odwołanie do stanu obiektu), a nie przechwytywanie zmiennej lokalnej.
6. Porównanie z klasami anonimowymi
Przed pojawieniem się wyrażeń lambda w Javie domknięcia można było tworzyć za pomocą klas anonimowych:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
Zasady są te same: zmienna word musi być final albo efektywnie finalna.
Różnica: zasięg this
- W klasie anonimowej this odnosi się do instancji tej klasy anonimowej.
- W wyrażeniu lambda this odnosi się do obiektu zewnętrznego (np. bieżącej instancji klasy).
7. Domknięcia a pola klasy
Jeśli lambda używa pola klasy, to w ścisłym sensie nie jest to „przechwytywanie zmiennej lokalnej” — pole jest zawsze dostępne i można je zmieniać.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("Licznik: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // Licznik: 1
r.run(); // Licznik: 2
}
}
8. Typowe błędy i cechy domknięć w Javie
Błąd nr 1: próba zmieniania zmiennej przechwyconej przez lambdę. Najczęstszy błąd — próba zmiany zmiennej lokalnej po tym, jak została użyta w lambdzie. Kompilator zgłosi: Variable used in lambda expression should be final or effectively final.
Błąd nr 2: oczekiwanie, że zmienna jest „zamrożona”. W Javie przechwycona zmienna to nie kopia wartości, lecz odwołanie do oryginału, gdy mówimy o polu klasy. Jeśli pole klasy się zmienia, lambda zobaczy nową wartość. Natomiast zmienne lokalne w lambdzie muszą być wyłącznie efektywnie finalne.
Błąd nr 3: oczekiwanie, że lambda tworzy nowy zasięg dla this. W lambdzie this to obiekt zewnętrzny (klasa otaczająca). W klasie anonimowej this to sama klasa anonimowa.
Błąd nr 4: używanie obiektów modyfikowalnych. Jeśli przechwytujesz referencję do obiektu modyfikowalnego (na przykład listy), możesz zmieniać jego zawartość wewnątrz lambdy, nawet jeśli sama zmienna jest efektywnie finalna:
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]
}
Tutaj zmienna list się nie zmienia (nie robimy list = ...), ale sam obiekt, do którego prowadzi referencja, jest modyfikowany.
GO TO FULL VERSION