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 – https://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

Your email address will not be published.


*