Изменение локальной переменной внутри lambda в Java
Быстрый ответ
В лямбда-выражениях Java прямые изменения локальных переменных невозможны, так как они должны быть фактически неизменяемы. Решить эту задачу можно с помощью использования массивов или специальных классов, вроде AtomicInteger
:
AtomicInteger counter = new AtomicInteger();
stream.forEach(item -> counter.incrementAndGet()); // Cчитаем овец в стаде
С таким подходом изменяется значение, заключённое в объекте, при этом сама ссылка на объект остаётся неизменной.
Обход ограничений
Использование классов-обёрток
Неизменяемую локальную переменную можно поместить в контейнер, и тогда получится создать иллюзию изменяемости:
AtomicReference<String> sneaky = new AtomicReference<>("initial");
stream.forEach(item -> sneaky.set(item)); // Замена значения производится с помощью обёртки
Манипуляции с массивами
Вы можете использовать массивы, чтобы изменить данные, не нарушая при этом константности ссылки:
final String[] wrapper = new String[1];
wrapper[0] = "initial";
stream.forEach(item -> wrapper[0] = item); // Меняем содержимое массива, но ссылка остаётся прежней
Также возможно создать собственный класс-контейнер для более гибкого и индивидуализированного подхода.
Анонимные классы и var
в Java 10+
С началом эры var
в Java 10, стали доступны анонимные классы для инкапсуляции изменяемого состояния:
var chameleon = new Object() { int count = 0; };
stream.forEach(item -> chameleon.count++); // Изменяем состояние внутри анонимного класса
Потоковая итерация с использованием IntStream.range
Изменение элементов в коллекции можно производить стильно, применяя потоковую итерацию:
IntStream.range(0, array.length).forEach(i -> array[i]++); // Усовершенствуем процесс итерации
Потокобезопасность при работе с параллельными потоками
При работе с параллельными потоками крайне важно учесть потокобезопасность:
ConcurrentHashMap map = new ConcurrentHashMap();
// Много поваров могут испортить бульон
Сведение: стратегия агрегирования
В определённых ситуациях вместо изменения локальных переменных предпочтительнее применять стратегии сведения, собирая результаты в один итоговый показатель:
int sum = IntStream.range(1, 10).reduce(0, Integer::sum); // Консолидируем результаты в единое целое
Визуализация
Представьте, что лямбда — это песочница, где можно модифицировать игрушки, но вынести их оттуда — невозможно.
Локальная переменная (снаружи):
🏠 [🚗🚕] (Машины оставляем дома, из песочницы их не вынесешь)
Лямбда (внутри песочницы):
() -> {
// Здесь можно перекрасить 🚗,
// но новую из дома 🏠 принести не получится.
}
После лямбды:
🏠 [🚗🚕] (Те же машинки, только возможно, перекрашенные)
Важно: Мы можем изменить внешний вид игрушки, но заменить саму игрушку — нет.
Правильно ли вы задаёте вопрос?
Порой желание изменить локальную переменную в лямбда может быть признаком проблемы XY. Возможно, стоит пересмотреть задачу в поисках более упрощённого и элегантного решения.
Размышления за рамками
Если AtomicInteger
или AtomicReference
не подходят для ваших целей, стоит подумать о возможности организации изменяемого состояния в отдельном классе или применение коллекторов для избегания необходимости изменений локальных переменных.
Cледование лучшим практикам
Лямбда-выражения наиболее эффективно работают с неизменяемым состоянием, и зачастую простой и легко поддерживаемый код достигается посредством отказа от мутаций.
Полезные материалы
- Лямбда-выражения (учебные материалы по Java™) – принципы лямбда-выражений в Java.
- Изменение локальной переменной внутри лямбда – Stack Overflow – обсуждение изменения локальных переменных в лямбда-выражениях.
- Java SE 8: Быстрый старт с лямбда – краткое ознакомление с использованием лямбда в Java 8.
- Документация JDK 21 – Главная — информация о новейших функциях Java, включая лямбда-выражения.
- Понимание синтаксиса лямбда Java 8 – подробный анализ синтаксиса лямбда-выражений Java 8.