Warum müssen in Lambdas verwendete lokale Variablen endgültig oder effektiv endgültig sein?

1. Einleitung

Java 8 gibt uns Lambdas und durch Assoziation den Begriff der effektiv endgültigen Variablen. Haben Sie sich jemals gefragt, warum in Lambdas erfasste lokale Variablen endgültig oder effektiv endgültig sein müssen?

Nun, das JLS gibt uns einen kleinen Hinweis, wenn es heißt: "Die Beschränkung auf effektiv endgültige Variablen verbietet den Zugriff auf sich dynamisch ändernde lokale Variablen, deren Erfassung wahrscheinlich zu Parallelitätsproblemen führen würde." Aber was bedeutet es?

In den nächsten Abschnitten werden wir uns eingehender mit dieser Einschränkung befassen und herausfinden, warum Java sie eingeführt hat. Wir werden Beispiele zeigen, um zu demonstrieren, wie sich dies auf Single-Threaded- und gleichzeitige Anwendungen auswirkt , und wir werden auch ein allgemeines Anti-Pattern entlarven, um diese Einschränkung zu umgehen.

2. Lambdas erfassen

Lambda-Ausdrücke können Variablen verwenden, die in einem äußeren Bereich definiert sind. Wir bezeichnen diese Lambdas als Gefangennahme von Lambdas . Sie können statische Variablen, Instanzvariablen und lokale Variablen erfassen, aber nur lokale Variablen müssen endgültig oder effektiv endgültig sein.

In früheren Java-Versionen ist dies aufgetreten, als eine anonyme innere Klasse eine lokale Variable für die umgebende Methode erfasst hat. Wir mussten das letzte Schlüsselwort vor der lokalen Variablen hinzufügen , damit der Compiler zufrieden ist.

Als ein bisschen syntaktischer Zucker kann der Compiler jetzt Situationen erkennen, in denen sich das endgültige Schlüsselwort zwar nicht befindet, die Referenz jedoch überhaupt nicht ändert, was bedeutet, dass es effektiv endgültig ist. Wir könnten sagen, dass eine Variable effektiv endgültig ist, wenn sich der Compiler nicht beschweren würde, wenn wir sie für endgültig erklären würden.

3. Lokale Variablen bei der Erfassung von Lambdas

Einfach ausgedrückt, dies wird nicht kompiliert:

Supplier incrementer(int start) { return () -> start++; }

start ist eine lokale Variable, und wir versuchen, sie innerhalb eines Lambda-Ausdrucks zu ändern.

Der Hauptgrund dafür, dass dies nicht kompiliert wird, ist, dass das Lambda den Wert von start erfasst , dh eine Kopie davon erstellt. Um die Variable Erzwingen final vermeidet den Eindruck, dass Erhöhen Start tatsächlich innerhalb der Lambda könnte die ändern Startmethodenparameter.

Aber warum macht es eine Kopie? Beachten Sie, dass wir das Lambda von unserer Methode zurückgeben. Somit wird das Lambda erst nach den fahren werden Startmethodenparameter werden Müll gesammelt. Java muss eine Kopie von start erstellen, damit dieses Lambda außerhalb dieser Methode lebt.

3.1. Parallelitätsprobleme

Für Spaß, lassen Sie uns für einen Moment vorstellen , dass Java haben lokale Variablen ermöglichen, irgendwie ihre erfassten Werte verbunden bleiben.

Was sollen wir hier tun:

public void localVariableMultithreading() { boolean run = true; executor.execute(() -> { while (run) { // do operation } }); run = false; }

Dies sieht zwar unschuldig aus, hat aber das heimtückische Problem der „Sichtbarkeit“. Denken Sie daran, dass jeder Thread seinen eigenen Stapel erhält. Wie stellen wir also sicher, dass unsere while- Schleife die Änderung der Ausführungsvariablen im anderen Stapel sieht ? Die Antwort in anderen Kontexten könnte die Verwendung synchronisierter Blöcke oder des flüchtigen Schlüsselworts sein.

Da Java jedoch die endgültige Einschränkung auferlegt, müssen wir uns über solche Komplexitäten keine Gedanken machen.

4. Statische oder Instanzvariablen beim Erfassen von Lambdas

Die vorherigen Beispiele können einige Fragen aufwerfen, wenn wir sie mit der Verwendung statischer oder Instanzvariablen in einem Lambda-Ausdruck vergleichen.

Wir können unser erstes Beispiel der Kompilierung machen nur durch unsere Umwandlung Start Variable in einer Instanzvariablen:

private int start = 0; Supplier incrementer() { return () -> start++; }

Aber warum können wir hier den Wert von start ändern ?

Einfach ausgedrückt geht es darum, wo Mitgliedsvariablen gespeichert werden. Lokale Variablen befinden sich auf dem Stapel, Mitgliedsvariablen jedoch auf dem Heap. Da es sich um Heapspeicher handelt, kann der Compiler garantieren, dass das Lambda auf den neuesten Startwert zugreifen kann .

Wir können unser zweites Beispiel beheben, indem wir dasselbe tun:

private volatile boolean run = true; public void instanceVariableMultithreading() { executor.execute(() -> { while (run) { // do operation } }); run = false; }

Die Ausführungsvariable ist jetzt für das Lambda sichtbar, auch wenn sie in einem anderen Thread ausgeführt wird, da wir das flüchtige Schlüsselwort hinzugefügt haben .

Allgemein gesprochen, wenn eine Instanzvariable erfassen, könnten wir daran denken , wie die letzte Variable Erfassung dieses . Wie dem auch sei, die Tatsache , dass der Compiler beschwert sich nicht , bedeutet nicht , dass wir nicht Vorsichtsmaßnahmen ergreifen sollten, insbesondere Umgebungen in Multithreading.

5. Vermeiden Sie Problemumgehungen

Um die Einschränkung lokaler Variablen zu umgehen, könnte jemand daran denken, Variablenhalter zu verwenden, um den Wert einer lokalen Variablen zu ändern.

Sehen wir uns ein Beispiel an, in dem ein Array zum Speichern einer Variablen in einer Single-Threaded-Anwendung verwendet wird:

public int workaroundSingleThread() { int[] holder = new int[] { 2 }; IntStream sums = IntStream .of(1, 2, 3) .map(val -> val + holder[0]); holder[0] = 0; return sums.sum(); }

Wir könnten denken, dass der Stream 2 zu jedem Wert summiert, aber er summiert tatsächlich 0, da dies der letzte verfügbare Wert ist, wenn das Lambda ausgeführt wird.

Gehen wir noch einen Schritt weiter und führen die Summe in einem anderen Thread aus:

public void workaroundMultithreading() { int[] holder = new int[] { 2 }; Runnable runnable = () -> System.out.println(IntStream .of(1, 2, 3) .map(val -> val + holder[0]) .sum()); new Thread(runnable).start(); // simulating some processing try { Thread.sleep(new Random().nextInt(3) * 1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } holder[0] = 0; }

Welchen Wert summieren wir hier? Es hängt davon ab, wie lange unsere simulierte Verarbeitung dauert. Wenn es kurz genug ist, um die Ausführung der Methode beenden zu lassen, bevor der andere Thread ausgeführt wird, wird 6 ausgegeben, andernfalls wird 12 ausgegeben.

Im Allgemeinen sind diese Problemumgehungen fehleranfällig und können zu unvorhersehbaren Ergebnissen führen. Daher sollten wir sie immer vermeiden.

6. Fazit

In diesem Artikel haben wir erklärt, warum Lambda-Ausdrücke nur endgültige oder effektiv endgültige lokale Variablen verwenden können. Wie wir gesehen haben, beruht diese Einschränkung auf der unterschiedlichen Natur dieser Variablen und darauf, wie Java sie im Speicher speichert. Wir haben auch die Gefahren einer gemeinsamen Problemumgehung aufgezeigt.

Wie immer ist der vollständige Quellcode für die Beispiele auf GitHub verfügbar.