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 = ...), но сам объект внутри неё меняется.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ