Что такое effectively final и что с ним делать



Начну с правильного ответа на вопрос выше. В точке Б мы получим предупреждение компилятора: local variables referenced from a lambda expression must be final or effectively final



В этом посте обсудим, что означает effectively final, о чём молчит спецификация и как менять переменные внутри лямбд.



Про модификатор final всё понятно — он запрещает изменение переменной



final int count = 100;



count всегда будет равен 100. Каждый, кто напишет



count = 200;



будет осуждён компилятором. Для ссылок схема такая же:



final User admin = User.createAdmin();



Ссылка admin всегда будет указывать на объект User с параметрами админа. Никто не может её переприсвоить:



 admin = new User(…)



Effectively final называется переменная, значение которой не меняется после инициализации. По сути это тот же final, но без ключевого слова.



Чтобы компилятор не ругался, надо выполнить два условия:



1️⃣ Локальная переменная однозначно определена до начала лямбда-выражения



Так не скомпилируется:

int x;

if (…) х = 10



Вот так норм:

int x;

if (…) х = 10; else х = 15;



2️⃣ Переменная не меняется внутри лямбды и после неё



int х = 10;

…лямбда…

х = 15



User user = …

…лямбда…

user = userRepository.findByName(…)

user.setTIN(…)



Зачем нужно такое ограничение?



JLS 15.27.2 говорит, что ограничение помогает избежать многопоточных проблем: The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems



С первого взгляда звучит разумно. Основное применение лямбд — в рамках Stream API. В Stream API есть опция parallel(), которая запускает выполнение в разных потоках. Там и возникнут concurrency problems.



Но я не принимаю это объяснение, потому что:



🤔 С каких пор компилятор волнуют многопоточные проблемы? Вся многопоточка отдана под контроль разработчика с начала времён



🤔 Если локальная переменная станет полем класса, то компилятор перестанет ругаться. При этом вероятность concurrency problems увеличится в разы



Моя гипотеза: требование final/effectively final связано с особенностями реализации лямбд и ограничением модели памяти. Это технические сложности в JVM и ничего больше. Отсутствие многопоточных проблем, о которых говорится в JLS, это всего лишь следствие, а не причина.



Как же менять переменные внутри лямбд?



1️⃣ Сделать переменную полем класса:



int count;

public void m() {

list.forEach(v -> count++);

}



Не лучший вариант, переменная доступна теперь другим потокам. Concurrency problems!



2️⃣ Использовать Atomic обёртку



Для примитивов:

AtomicInteger count = new AtomicInteger(0);

list.forEach(v -> count.incrementAndGet())



Для ссылок:

AtomicReference<User> user = new AtomicReference<>();

…map(i -> user.set(…))



3️⃣ Использовать массив с одним элементом



int[] res = new int[] {0};

list.forEach(v -> res[0]++);



Популярный вариант, который подходит и для примитивов, и для ссылок. Но мне больше нравится вариант с Atomic:)