1. Знайомство із замиканнями
Замикання — це функція (або об’єкт-функція), яка не лише використовує свої параметри, а й «запам’ятовує» змінні з навколишнього контексту, де її було створено. Простіше кажучи, якщо лямбда-вираз або анонімний клас усередині методу використовує змінні з цього методу — така конструкція стає замиканням.
Приклад «на пальцях»
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "Привіт, ";
Runnable sayHello = () -> System.out.println(greeting + "світе!");
sayHello.run(); // Виведе: Привіт, світе!
}
}
Тут лямбда «захопила» змінну greeting із зовнішнього методу і використовує її всередині себе. Це й є замикання!
2. Ефективно final змінні: що це й навіщо?
У Java лямбда-вирази (і анонімні класи) можуть використовувати лише такі змінні із зовнішнього методу, які оголошені як final або не змінюються після ініціалізації. Такі змінні називаються ефективно final.
Чому так?
Це обмеження пов’язане з тим, що лямбда-вираз може бути викликаний після завершення роботи методу, у якому його було створено. Якби змінну можна було змінювати, виникла б плутанина: яку версію змінної використовувати? Щоб уникнути сюрпризів, Java вимагає, щоб змінна була незмінною (або принаймні виглядала незмінною).
Приклад: коректне використання
public static void main(String[] args) {
int number = 42; // number — ефективно final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
Приклад: спроба змінити змінну після використання
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // ПОМИЛКА: змінна number має бути final або ефективно final
r.run();
}
Компілятор видасть помилку: Variable used in lambda expression should be final or effectively final.
Ефективно final — це… змінна, якій присвоюють рівно один раз і далі не змінюють. Не обов’язково писати final, компілятор сам здогадається.
3. Як лямбда-вирази захоплюють змінні?
Коли ви пишете лямбду, що використовує зовнішню змінну, Java «упаковує» цю змінну разом із лямбдою. Навіть якщо метод, де було створено лямбду, уже завершився, змінна не зникає — вона живе всередині замикання.
Ілюстрація: лямбда «запам’ятовує» змінну
public static Runnable createGreeter(String name) {
// name — параметр методу, його буде захоплено лямбдою
return () -> System.out.println("Привіт, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Вася");
greeter.run(); // Привіт, Вася!
}
Тут змінна name уже не існує в стеці методу main, але greeter усе ще «пам’ятає» її значення.
Як це реалізовано всередині?
Компілятор Java створює спеціальний допоміжний об’єкт (його ще називають «capture/display class»), який зберігає всі захоплені змінні. Лямбда-вираз стає об’єктом, у якого є посилання на цей «контейнер» зі змінними.
4. Приклад замикання: повертаємо функцію, що використовує змінну
Напишемо функцію, яка повертає лямбду, що використовує змінну зі свого контексту:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x — захоплюється лямбдою
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
Тут змінна x уже вийшла зі стека методу, але лямбда усе ще може її використовувати.
5. Чому не можна змінювати захоплені змінні?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // ПОМИЛКА!
printer.run();
}
Компілятор не дозволить це зробити. Якби ми могли змінювати base, стало б незрозуміло, яку версію змінної використовувати в лямбді: стару чи нову? Тому Java забороняє змінювати локальні змінні, захоплені лямбдою.
Що можна використовувати в лямбді?
- Локальні змінні, які не змінюються після ініціалізації (ефективно final).
- Поля класу (як static, так і нестатичні) — їх можна змінювати, але це вже інший механізм (звернення до стану об’єкта), а не захоплення локальної змінної.
6. Порівняння з анонімними класами
До появи лямбда-виразів у Java можна було робити замикання за допомогою анонімних класів:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
Правила ті самі: змінна word має бути final або ефективно final.
Відмінність: область видимості this
- В анонімному класі this посилається на екземпляр анонімного класу.
- У лямбда-виразі this посилається на зовнішній об’єкт (наприклад, поточний екземпляр класу).
7. Замикання та поля класу
Якщо лямбда використовує поле класу, це не «захоплення локальної змінної» у строгому сенсі — поле завжди доступне, і його можна змінювати.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("Лічильник: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // Лічильник: 1
r.run(); // Лічильник: 2
}
}
8. Типові помилки й особливості замикань у Java
Помилка № 1: спроба змінювати змінну, захоплену лямбдою. Найчастіша помилка — спробувати змінити локальну змінну після того, як її використано в лямбді. Компілятор повідомить: Variable used in lambda expression should be final or effectively final.
Помилка № 2: очікування, що змінну «заморожено». У Java захоплена змінна — це не копія значення, а посилання на оригінал, коли йдеться про поле класу. Якщо поле класу змінюється, лямбда побачить нове значення. Але локальні змінні в лямбді зобов’язані бути лише ефективно final.
Помилка № 3: очікування, що лямбда створює нову область видимості для this. У лямбді this — це зовнішній об’єкт (контейнерний клас). В анонімному класі this — це сам анонімний клас.
Помилка № 4: використання змінюваних об’єктів. Якщо ви захоплюєте посилання на змінюваний об’єкт (наприклад, список), ви можете змінювати його вміст усередині лямбди, навіть якщо сама змінна ефективно final:
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]
}
Тут змінна list не змінюється (ми не робимо list = ...), але сам об’єкт усередині неї змінюється.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ