При захвате локальной переменной лямбдой в Stream почему компилятор требует, чтобы она не изменялась после инициализации?
Локальная переменная, захваченная лямбдой, должна быть final или эффективно final: после присваивания её значение нельзя переназначить. Лямбда получает значение локальной переменной, а не полноценную изменяемую локальную область видимости, поэтому Java запрещает неоднозначную модель совместного изменения такой переменной.
Это требование касается именно переназначения ссылки или значения. Оно не делает объект, на который указывает переменная, неизменяемым и само по себе не гарантирует потокобезопасность.
Лямбды появились в Java для передачи поведения в виде значения: например, функции преобразования или условия для операции Stream. При этом локальные переменные метода обычно существуют только в рамках его выполнения, тогда как лямбда может быть вызвана позже.
Поэтому Java использует механизм захвата значения локальной переменной. Требование эффективной неизменяемости сохраняет однозначную семантику захвата и согласуется с ограничениями, которые ранее применялись к локальным переменным в анонимных классах.
Если бы локальную переменную можно было переназначать после создания лямбды, возникал бы вопрос: должна ли лямбда видеть старое значение, новое значение или общее изменяемое хранилище. В последовательном и особенно в параллельном Stream это добавило бы скрытое состояние и усложнило бы рассуждение о результате.
Запрет переназначения не защищает от всех проблем. Можно захватить неизменяемую ссылку на изменяемый объект и менять его содержимое; тогда появляются обычные риски гонок данных, нарушения порядка и зависимости от момента выполнения лямбды.
Переменная считается эффективно final, если ей присвоено значение один раз и после этого она не переназначается. Явное слово final не обязательно: компилятор сам проверяет это условие при анализе лямбды.
В примере лямбда захватывает suffix, но переменная после инициализации не меняется. Если попытаться присвоить ей другое значение после создания лямбды или до его использования, программа не скомпилируется, потому что переменная перестанет быть эффективно final.
Важно различать переменную и объект. Если захвачена ссылка на List, нельзя переназначить саму ссылку, но технически можно добавить элементы в список. Это не нарушает правило эффективной неизменяемости ссылки, однако может сделать результат зависимым от внешнего состояния.
Для передачи изменяемого состояния лучше явно использовать параметры функций, локальную агрегацию через стандартные операции Stream или потокобезопасные структуры там, где это действительно необходимо. Не следует превращать правило эффективной final в замену синхронизации: оно предотвращает переназначение локальной переменной, но не устанавливает отношения видимости между потоками.
Сервис формирует подписи для набора идентификаторов, а формат подписи зависит от конфигурации текущего запроса. Вариант с изменяемой локальной переменной неудобен: попытка переключить формат после создания pipeline нарушает требование эффективной final, а общий изменяемый контейнер скрывает состояние от читателя кода.
Можно передать формат как параметр отдельного метода для каждого запуска Stream. Плюс такого решения — явные зависимости и отсутствие общего состояния; минус — иногда потребуется дополнительный метод или объект контекста.
Другой вариант — хранить формат в поле объекта. Это позволяет менять конфигурацию, но при параллельной обработке требует отдельно решать вопросы видимости и синхронизации; кроме того, лямбда начинает зависеть от внешнего состояния объекта.
Предпочтительно зафиксировать конфигурацию в локальной переменной один раз и захватить её лямбдой. Такой вариант проще проверять, безопаснее для повторного выполнения pipeline и не создаёт скрытой конкуренции. Если конфигурация должна меняться, изменение следует выполнить до построения нового pipeline или передать нужное значение явно.
Нет. Она запрещает переназначение локальной переменной, но не гарантирует безопасную публикацию объекта и не защищает его внутреннее изменяемое состояние. Например, неизменяемая ссылка на обычный изменяемый список всё ещё может приводить к гонкам при одновременной модификации.
Лямбда захватывает не отдельное локальное значение поля, а ссылку на объект, обычно представленную через this. Поле является состоянием объекта и не подпадает под правило эффективной final для локальных переменных. Однако изменение поля из Stream всё равно может нарушить потокобезопасность и сделать результат зависимым от порядка выполнения.
Да, например ссылка на список может оставаться неизменной, пока его содержимое меняется. Компилятор проверяет только переназначение ссылки, а не операции над объектом. Такой приём допустим лишь при явно обеспеченной безопасности доступа; в большинстве задач лучше возвращать результат через операции Stream, а не изменять общий контейнер из лямбды.