CodeGym /Kursy /JAVA 25 SELF /Domknięcia (closures) w Javie: specyfika

Domknięcia (closures) w Javie: specyfika

JAVA 25 SELF
Poziom 48, Lekcja 3
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 48, lekcja 3
Niedostępne
Programowanie uprzejmego robota-witającego 🤖
Programowanie uprzejmego robota-witającego 🤖
1
Zadanie
JAVA 25 SELF, poziom 48, lekcja 3
Niedostępne
Tablica myśli dla zespołu 🧠
Tablica myśli dla zespołu 🧠
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION