Какое требование предъявляется к локальной переменной, захватываемой лямбда-выражением в Java?
Захватываемая лямбдой локальная переменная должна быть объявлена как final либо оставаться effectively final — не изменяться после инициализации. Если переменная была переназначена, компилятор не разрешит использовать её внутри лямбды.
Это правило относится к локальным переменным, параметрам методов и параметрам блоков обработки исключений. Оно не запрещает изменять состояние объекта, на который ссылается захваченная переменная.
Лямбда-выражения появились в Java 8 как средство компактного описания поведения, например обработчиков и функций для Stream API. Лямбда может быть выполнена не в момент создания, а позже, поэтому ей требуется безопасный способ обращаться к значениям из внешнего метода.
До появления лямбд похожее ограничение действовало для локальных переменных, используемых в анонимных классах: такие переменные должны были быть явно объявлены как final. Java 8 сохранила ограничение, но разрешила не писать ключевое слово final, если компилятор может доказать, что переменная фактически не изменяется.
Локальная переменная обычно существует в рамках вызова метода, а лямбда может быть сохранена и выполнена после завершения этого вызова. Например, обработчик может попасть в очередь или быть передан другому потоку.
Если бы лямбда напрямую зависела от изменяемой локальной переменной, поведение зависело бы от момента выполнения: использовать начальное значение или последнее присвоенное? Java устраняет такую неоднозначность, разрешая захватывать только неизменяемую привязку переменной.
Effectively final означает, что переменная не объявлена с final, но после инициализации ей не присваивается новое значение. Однократная инициализация допустима; повторное присваивание, оператор увеличения или уменьшения значения уже нарушают требование.
В примере number является effectively final, поэтому лямбда может его захватить. Если после инициализации присвоить number другое значение, компиляция завершится ошибкой.
Концептуально лямбда получает значение захваченной локальной переменной при создании. Она не получает свободный доступ к будущему стековому слоту метода и не наблюдает последующие присваивания локальной переменной. Именно поэтому компилятор запрещает переназначение.
Ограничение касается самой переменной, а не объекта по ссылке. Ссылка может быть effectively final, хотя внутреннее состояние объекта изменяется. Например, захваченный список можно пополнять, но нельзя заменить переменную новым списком.
Это не делает объект неизменяемым и не обеспечивает потокобезопасность. Если один и тот же изменяемый объект используется несколькими потоками, синхронизация или другие средства обеспечения видимости и атомарности остаются ответственностью разработчика.
Поля объекта и статические поля не являются локальными переменными и не подпадают под это правило. Лямбда может обращаться к ним, но при этом возможные изменения их состояния регулируются обычными правилами доступа к полям.
Сервис формирует отложенные задачи для обработки заказов. Разработчик пытается использовать локальный счётчик цикла внутри лямбды и затем изменять его на каждой итерации. Такой вариант не компилируется, потому что счётчик не является effectively final.
Первый вариант — захватить изменяемый контейнер, например массив из одного элемента. Это позволяет обойти ограничение, но ухудшает читаемость, создаёт изменяемое общее состояние и не решает проблемы многопоточного доступа.
Второй вариант — передавать текущее значение в отдельный метод, который создаёт лямбду с уже зафиксированным параметром. Этот подход явно показывает границу захвата и не требует изменяемого контейнера.
Предпочтительным является второй вариант: каждая задача получает собственное неизменяемое значение, поэтому порядок выполнения задач не меняет их входные данные. Результат — предсказуемое поведение и отсутствие искусственного состояния, добавленного только ради обхода ограничения компилятора.
Да. final запрещает изменить ссылку, но не запрещает изменить объект, на который она указывает. Поэтому финальный список можно дополнить элементами, если сам список допускает изменение.
Однако это не означает потокобезопасность. При совместном использовании такого объекта несколькими потоками возможны гонки данных, несогласованные результаты и проблемы видимости.
Effectively final определяется правилами компилятора, а не фактическим использованием переменной. Если после инициализации в исходном коде есть повторное присваивание, переменная не считается effectively final, даже если это присваивание логически не выполняется или переменная больше нигде не читается.
Например, присваивание внутри условной ветки всё равно является потенциальным изменением переменной с точки зрения анализа компилятора. Такая переменная не может быть захвачена лямбдой.
Параметр является локальной переменной метода и подчиняется тому же правилу. Если после получения параметра ему не присваивается новое значение, он считается effectively final и доступен внутри лямбды.
Если параметр переназначить, например нормализовать его новым присваиванием, захват этой переменной станет недопустимым. Решение — создать отдельную переменную с итоговым значением и не изменять её после инициализации.