Lambdas: lokale Variablen müssen final sein, Instanzvariablen nicht

In einem Lambda müssen lokale Variablen final sein, während Instanzvariablen dies nicht tun. Warum?

Der grundlegende Unterschied zwischen einem Feld und einer lokalen Variablen besteht darin, dass die lokale Variable kopiert wird, wenn JVM eine Lambda-Instanz erstellt. Andererseits können Felder frei geändert werden, da die Änderungen an ihnen auch an die äußere classninstanz weitergegeben werden (ihr scope ist die gesamte äußere class, wie Boris unten ausgeführt hat).

Die einfachste Methode, über anonyme classn, Closures und Labmdas nachzudenken, besteht in der Perspektive des Variablenbereichs . Stellen Sie sich einen Kopierkonstruktor für alle lokalen Variablen vor, die Sie an einen Abschluss übergeben.

Im Dokument des Projektes Lambda: Status des Lambda v4

Unter Abschnitt 7. Variable Capture , wird erwähnt, dass ….

Es ist unsere Absicht, die Erfassung veränderbarer lokaler Variablen zu verbieten. Der Grund ist, dass Idiome wie folgt:

int sum = 0; list.forEach(e -> { sum += e.size(); }); 

sind grundsätzlich seriell; Es ist ziemlich schwierig, solche Lambda-Körper zu schreiben, die keine Rassenbedingungen haben. Sofern wir nicht bereit sind – vorzugsweise zur Kompilierzeit – zu erzwingen, dass eine solche function ihren Capturing-Thread nicht verlassen kann, kann diese function möglicherweise mehr Probleme verursachen, als sie triggers.

Bearbeiten:

Eine andere Sache, die hier zu beachten ist, ist, dass lokale Variablen im Konstruktor der inneren class übergeben werden, wenn Sie auf sie innerhalb Ihrer inneren class zugreifen, und dies wird nicht mit nicht abschließenden Variablen funktionieren, da der Wert von nicht endgültigen Variablen nach der Konstruktion geändert werden kann.

Im Falle einer Instanzvariablen übergibt der Compiler den Verweis der class und die Referenz der class wird für den Zugriff auf die Instanzvariable verwendet. Dies ist bei Instanzvariablen nicht erforderlich.

PS: Es ist erwähnenswert, dass anonyme classn nur auf letzte lokale Variablen (in JAVA SE 7) zugreifen können, während Sie in Java SE 8 effektiv auf finale Variablen zugreifen können, auch innerhalb von Lambda-classn und inneren classn.

Da auf Instanzvariablen immer über eine some_expression.instance_variable auf eine Referenz auf ein Objekt some_expression.instance_variable . B. some_expression.instance_variable . Auch wenn Sie nicht wie in instance_variable explizit über die Punktnotation darauf zugreifen, wird sie implizit als this.instance_variable behandelt (oder wenn Sie in einer inneren class sind, die auf die Instanzvariable OuterClass.this.instance_variable einer äußeren class zugreift) die Haube this..instance_variable ).

Daher wird auf eine Instanzvariable niemals direkt zugegriffen, und die reale “Variable”, auf die Sie direkt zugreifen, ist this (die “effektiv endgültig” ist, da sie nicht zuweisbar ist) oder eine Variable am Anfang eines anderen Ausdrucks.

Es scheint, als ob Sie nach Variablen fragen, die Sie aus einem Lambda-Körper referenzieren können.

Aus der JLS §15.27.2

Alle verwendeten lokalen Variablen, Formalparameter oder Ausnahmeparameter, die in einem Lambda-Ausdruck nicht deklariert sind, müssen entweder als final deklariert werden oder als endgültig gelten (§4.12.4), oder es tritt ein Fehler bei der Kompilierung auf, wenn die Verwendung versucht wird.

Sie müssen also Variablen nicht als final deklarieren, Sie müssen nur sicherstellen, dass sie “effektiv endgültig” sind. Dies ist die gleiche Regel wie für anonyme classn.

In Java 8 im Action Book wird diese Situation wie folgt erklärt:

Sie fragen sich vielleicht, warum lokale Variablen diese Einschränkungen haben. Erstens gibt es einen entscheidenden Unterschied darin, wie Instanz- und lokale Variablen im Hintergrund implementiert werden. Instanzvariablen werden auf dem Heap gespeichert, während lokale Variablen auf dem Stapel gespeichert sind. Wenn ein Lambda direkt auf die lokale Variable zugreifen und das Lambda in einem Thread verwenden könnte, könnte der Thread, der das Lambda verwendet, versuchen, auf die Variable zuzugreifen, nachdem der Thread, der die Variable zugewiesen hatte, die Zuordnung aufgehoben hat. Daher implementiert Java den Zugriff auf eine freie lokale Variable als Zugriff auf eine Kopie und nicht als Zugriff auf die ursprüngliche Variable. Dies macht keinen Unterschied, wenn die lokale Variable nur einmal zugewiesen ist – also die Einschränkung. Zweitens verhindert diese Einschränkung auch typische imperative Programmiermuster (die, wie wir in späteren Kapiteln erklären werden, eine einfache Parallelisierung verhindern), die eine äußere Variable mutieren.

Innerhalb von Lambda-Ausdrücken können Sie effektiv endgültige Variablen aus dem umgebenden Bereich verwenden. Effektiv bedeutet, dass es nicht obligatorisch ist, die Variable final zu deklarieren, aber stellen Sie sicher, dass Sie ihren Status innerhalb der Lambda-Expression nicht ändern.

Sie können dies auch in closures verwenden, und “this” bedeutet das umschließende Objekt, nicht aber das Lambda selbst, da closures anonyme functionen sind und ihnen keine class zugeordnet ist.

Wenn Sie also ein beliebiges Feld (zB private Integer i;) aus der umschließenden class verwenden, das nicht als final und nicht als effektiv final deklariert ist, funktioniert es trotzdem, da der Compiler in Ihrem Auftrag den Trick macht und “this” einfügt (this.i) .

 private Integer i = 0; public void process(){ Consumer c = (i)-> System.out.println(++this.i); c.accept(i); } 

Hier ist ein Codebeispiel, da ich das auch nicht erwartet habe, erwartete ich, dass ich nichts außerhalb meines Lambda ändern kann

  public class LambdaNonFinalExample { static boolean odd = false; public static void main(String[] args) throws Exception { //boolean odd = false; - If declared inside the method then I get the expected "Effectively Final" compile error runLambda(() -> odd = true); System.out.println("Odd=" + odd); } public static void runLambda(Callable c) throws Exception { c.call(); } } 

Ausgabe: ungerade = wahr

JA, Sie können die Member-Variablen der Instanz ändern, aber Sie können die Instanz NICHT ändern, genauso wie Sie Variablen handhaben.

So etwas wie erwähnt:

  class Car { public String name; } public void testLocal() { int theLocal = 6; Car bmw = new Car(); bmw.name = "BMW"; Stream.iterate(0, i -> i + 2).limit(2) .forEach(i -> { // bmw = new Car(); // LINE - 1; bmw.name = "BMW NEW"; // LINE - 2; System.out.println("Testing local variables: " + (theLocal + i)); }); // have to comment this to ensure it's `effectively final`; // theLocal = 2; } 

Das Grundprinzip zur Beschränkung der lokalen Variablen besteht in der Gültigkeit von Daten und Berechnungen

Wenn das Lambda, bewertet durch den zweiten Thread, wurde die Fähigkeit gegeben, lokale Variablen zu mutieren. Selbst die Fähigkeit, den Wert veränderbarer lokaler Variablen aus einem anderen Thread zu lesen, würde die Notwendigkeit der Synchronisation oder die Verwendung von flüchtigen Daten einführen, um das Lesen veralteter Daten zu vermeiden.

Aber wie wir den Hauptzweck der Lambdas kennen

Unter den verschiedenen Gründen ist der wichtigste für die Java-Plattform, dass sie die Verarbeitung von Sammlungen über mehrere Threads verteilen.

Im Gegensatz zu lokalen Variablen kann die lokale Instanz mutiert werden, da sie global freigegeben ist . Wir können dies besser über den Heap- und Stack-Unterschied verstehen:

Jedes Mal, wenn ein Objekt erstellt wird, wird es immer im Heap-Space gespeichert, und der Stack-Speicher enthält den Verweis darauf. Der Stapelspeicher enthält nur lokale primitive Variablen und Referenzvariablen für Objekte im Heap-Speicher.

Um es zusammenzufassen, es gibt zwei Punkte, die ich wirklich wichtig finde:

  1. Es ist wirklich schwierig, die Instanz effektiv final zu machen, was eine Menge sinnloser Last verursachen kann (stell dir einfach die tief verschachtelte class vor);

  2. Die Instanz selbst ist bereits global freigegeben und Lambda ist auch unter Threads teilbar, so dass sie richtig zusammenarbeiten können, da wir wissen, dass wir die Mutation handhaben und diese Mutation weitergeben wollen;

Balance Point hier ist klar: Wenn Sie wissen, was Sie tun, können Sie es leicht tun, aber wenn nicht, wird die Standardeinschränkung helfen, heimtückische Bugs zu vermeiden.

PS Wenn die Synchronisation in der Instanzmutation benötigt wird, können Sie direkt die Stream-Reduktionsmethoden verwenden oder wenn es ein Abhängigkeitsproblem in der Instanzmutation gibt , können Sie dann thenApply oder thenCompose in Function während des mapping oder ähnlicher Methoden verwenden.

Einige Konzepte für zukünftige Besucher aufstellen:

Grundsätzlich läuft alles darauf hinaus, dass der Compiler deterministisch sagen kann, dass der Lambda-Ausdruck nicht an einer veralteten Kopie der Variablen arbeitet .

Im Falle von lokalen Variablen hat der Compiler keine Möglichkeit, sicher zu sein, dass der Lambda-Ausdruckskörper nicht mit einer veralteten Kopie der Variablen arbeitet, es sei denn, diese Variable ist endgültig oder effektiv final, also sollten lokale Variablen entweder final oder effektiv final sein.

Wenn Sie nun im Fall von Instanzfeldern auf ein Instanzfeld innerhalb des Lambda-Ausdrucks zugreifen, fügt der Compiler dem Variablenzugriff ein This hinzu (wenn Sie dies nicht explizit getan haben), und da this tatsächlich endgültig ist, ist der Compiler sicher, dass der Lambda-Ausdruck verwendet wird body wird immer die letzte Kopie der Variablen haben (bitte beachten Sie, dass Multithreading momentan für diese Diskussion nicht verfügbar ist). Im Fall von Instanzfeldern kann der Compiler feststellen, dass der Lambda-Text die letzte Kopie der Instanzvariable enthält, damit Instanzvariablen nicht endgültig oder effektiv final sein müssen. Bitte sehen Sie sich den folgenden Screenshot von einer Oracle Folie an:

Bildbeschreibung hier eingeben

Beachten Sie außerdem, dass Sie möglicherweise ein Problem haben, wenn Sie auf ein Instanzfeld in einem Lambda-Ausdruck zugreifen und dieser in einer Multithread-Umgebung ausgeführt wird.