Java 8 – final vs effectively final
Java 8 – final vs effectively final
Java 8 wprowadza nowy termin – „effectively final variable„. Zmienna oznaczona modyfikatorem final oraz której wartość jest przypisana tylko raz to zmienna typu „final„:
final int variable = 123;
Wszystkie zmienne w Java 8 traktowane są jako zmienne finalne. Pisząc:
int variable = 123
zmienna traktowana jest jako „effectively final„. Natomiast jeśli napiszemy:
int variable = 123; variable = 456;
zmienna jest typu „non final„.
Często spotykanym błędem podczas pracy z wyrażeniami lambda jest:
local variables referenced from a lambda expression must be final or effectively final
np. w poniżej zamieszczonym kodzie:
Supplier<Integer> incrementer(int start) { return () -> start++; }
Błąd podobny do tego kiedy w klasie anonimowej używano zmiennych lokalnych które nie były finalnymi.
Ze względu na problem związany z wielowątkowością (i nie tylko) opisany na stronie Oracle – http://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.27.2:
The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.
w wyrażeniach lambda używane zmienne muszą być wcześniej zadeklarowane jako final lub effectively final, dlaczego? Poniżej zamieszczony fragment kodu posiada na pierwszy rzut oka niewidoczny błąd związany z widocznością zmiennej logicznej run:
public void localVariableMultithreading() { boolean run = true; executor.execute(() -> { while (run) { // do operation } }); run = false; }
Inne wątki nie widzą, że zmienna lokalna run zmieniła swoją wartość ze względu na fakt, że każdy wątek posiada swój własny lokalny stos tzw. TLS – (z ang. Thread local storage) na którym odkładane są m.in. zmienne lokalne. Kiedy JVM tworzy wyrażenie lambda to zmienne lokalne są kopiowane. Dlaczego? W poniżej zamieszczonym kodzie widać, że wyrażenie lambda zwracane jest w funkcji to z kolei oznacza, że wyrażenie lambda musi być widoczne po za nią nawet wtedy jeśli garbage collector usunie zmienną używaną w funkcji lambda. Ponadto modyfikacja wartości w wyrażeniu lambda nie może mieć wpływu na argument przkazywany do metody!
Supplier<Integer> incrementer(int start) { return () -> start++; }
Java 8 zabezpiecza nas przed tego typu błędem nie pozwalając na użycie zmiennych które NIE są typu final lub effectively final w wyrażeniach lambda.
Co ciekawe jeśli zadeklarujemy zmienną Start NIE jako zmienną lokalną np.:
private int start = 0; Supplier<Integer> incrementer() { return () -> start++; }
to kompilator nie będzie informował o błędzie. Dlaczego? Zmienne zadeklarowane w klasie (z ang. members/instances variables) deklarowane są na stercie (z ang. Heap). Oznacza to, że w wyrażeniach lambda zawsze będzie dostępna ostatnia wartość danej zmiennej – kompilator to zagwarantuje – zamieni zapis:
start++
na zapis:
this.start++
, ale nie jest to rozwiązanie wszystkich problemów – co w przypadku pracy z wątkami? Spójrzmy na kod:
public static void main(String[] args) { int[] integers = new int[] { 2 }; Runnable runnable = () -> System.out.println(IntStream .of(4, 5, 6) .map(val -> val + integers[0]) .sum()); new Thread(runnable).start(); integers[0] = 0; }
Do wyrażenia lambda zostanie wzięta ostatnia wartość zmiennej czyli integers[0] = 0;
Wynik:
15
A jeśli dodamy chwilę czasu używając metody Thread.sleep()?:
public static void main(String[] args) { int[] integers = new int[] { 2 }; Runnable runnable = () -> System.out.println(IntStream .of(4, 5, 6) .map(val -> val + integers[0]) .sum()); new Thread(runnable).start(); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } integers[0] = 0; }
Wynik będzie następujący:
21
Takie działanie może doprowadzić do nieprzewidywalnych rezultatów. A co w przypadku kiedy wartość zmiennej może modyfikować wiele wątków? – należy użyć modyfikatora volatile. Modyfikator volatile zapewnia, że zmiana dokonana przez jeden wątek na współdzielonych danych będzie widoczna przez drugi wątek.
import static java.lang.Thread.sleep; interface Operation { void doOperation(); } class MyRunnable implements Runnable { private volatile boolean active; public void run() { active = true; while (active) { } Operation operation = () -> System.out.println(active); operation.doOperation(); } public void stop() { active = false; } } public class VolatileDemo { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); new Thread(myRunnable).start(); new Thread(new Runnable(){ @Override public void run() { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } myRunnable.stop(); } }).start(); } }
Wynik:
false
Leave a comment