In einem Lambda müssen lokale Variablen endgültig sein, Instanzvariablen jedoch nicht. Warum so?
Integer[] count = {new Integer(5)}
. H. Siehe auch stackoverflow.com/a/50457016/7154924 .
In einem Lambda müssen lokale Variablen endgültig sein, Instanzvariablen jedoch nicht. Warum so?
Integer[] count = {new Integer(5)}
. H. Siehe auch stackoverflow.com/a/50457016/7154924 .
Antworten:
Der grundlegende Unterschied zwischen einem Feld und einer lokalen Variablen besteht darin, dass die lokale Variable kopiert wird, wenn JVM eine Lambda-Instanz erstellt. Auf der anderen Seite können Felder frei geändert werden, da die Änderungen auch an die Instanz der externen Klasse weitergegeben werden (ihr Umfang ist die gesamte externe Klasse, wie Boris unten ausgeführt hat).
Die einfachste Art, über anonyme Klassen, Schließungen und Labmdas nachzudenken, ist aus der Perspektive des variablen Umfangs . Stellen Sie sich einen Kopierkonstruktor vor, der für alle lokalen Variablen hinzugefügt wurde, die Sie an einen Abschluss übergeben.
Im Dokument des Projekts Lambda: Zustand des Lambda v4
Unter Abschnitt 7. Variablenerfassung wird erwähnt, dass ....
Es ist unsere Absicht, die Erfassung veränderlicher lokaler Variablen zu verbieten. Der Grund ist, dass Redewendungen wie diese:
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 Rennbedingungen haben. Sofern wir nicht bereit sind, - vorzugsweise zur Kompilierungszeit - zu erzwingen, dass eine solche Funktion ihrem Erfassungsthread nicht entkommen kann, kann diese Funktion mehr Probleme verursachen als sie löst.
Bearbeiten:
Eine andere Sache, die hier zu beachten ist, ist, dass lokale Variablen im Konstruktor der inneren Klasse übergeben werden, wenn Sie innerhalb Ihrer inneren Klasse auf sie zugreifen. Dies funktioniert nicht mit nicht endgültigen Variablen, da der Wert von nicht endgültigen Variablen nach der Konstruktion geändert werden kann.
Während im Fall einer Instanzvariablen der Compiler die Referenz der Klasse übergibt und die Referenz der Klasse verwendet wird, um auf die Instanzvariable zuzugreifen. Bei Instanzvariablen ist dies also nicht erforderlich.
PS: Es ist erwähnenswert, dass anonyme Klassen nur auf endgültige lokale Variablen zugreifen können (in JAVA SE 7), während Sie in Java SE 8 effektiv auf endgültige Variablen auch innerhalb von Lambda sowie inneren Klassen zugreifen können.
In Java 8 im Aktionsbuch wird diese Situation wie folgt erklärt:
Möglicherweise fragen Sie sich, warum für lokale Variablen diese Einschränkungen gelten. Erstens gibt es einen entscheidenden Unterschied darin, wie Instanz- und lokale Variablen hinter den Kulissen 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 kann und das Lambda in einem Thread verwendet wird, kann der Thread, der das Lambda verwendet, versuchen, auf die Variable zuzugreifen, nachdem der Thread, der die Variable zugewiesen hat, die Zuordnung aufgehoben hat. Daher implementiert Java den Zugriff auf eine kostenlose lokale Variable als Zugriff auf eine Kopie davon und nicht als Zugriff auf die ursprüngliche Variable. Dies macht keinen Unterschied, wenn die lokale Variable nur einmal zugewiesen wird - daher die Einschränkung. Zweitens entmutigt diese Einschränkung auch typische imperative Programmiermuster (die, wie wir in späteren Kapiteln erläutern,
ForkJoin
, gibt es eine Kopie für verschiedene Threads, und eine Mutation in Lambdas ist theoretisch akzeptabel. In diesem Fall ist dies möglich werden mutiert . Aber die Sache hier anders ist, lokale Variablen in Lambda verwendet werden , sind für die Parallelisierung erreicht durch parallelStream
, und diese lokalen Variablen werden von verschiedenen Threads gemeinsam genutzt , die auf dem basieren lambda .
Da auf Instanzvariablen immer über eine Feldzugriffsoperation für eine Referenz auf ein Objekt zugegriffen wird, d some_expression.instance_variable
. H. Selbst wenn Sie nicht explizit über die Punktnotation darauf zugreifen instance_variable
, wird dies implizit als behandelt this.instance_variable
(oder wenn Sie sich in einer inneren Klasse befinden, die auf die Instanzvariable einer äußeren Klasse zugreift OuterClass.this.instance_variable
, die sich unter der Haube befindet this.<hidden reference to outer this>.instance_variable
).
Daher wird niemals direkt auf eine Instanzvariable zugegriffen, und die reale "Variable", auf die Sie direkt zugreifen, ist this
(was "effektiv endgültig" ist, da sie nicht zuweisbar ist) oder eine Variable am Anfang eines anderen Ausdrucks.
Einige Konzepte für zukünftige Besucher aufstellen:
Grundsätzlich läuft alles auf den Punkt hinaus, an dem der Compiler deterministisch erkennen sollte, dass der Lambda-Ausdruckskörper nicht an einer veralteten Kopie der Variablen arbeitet .
Bei lokalen Variablen kann der Compiler nicht sicherstellen, dass der Lambda-Ausdruckskörper nicht an einer veralteten Kopie der Variablen arbeitet, es sei denn, diese Variable ist endgültig oder effektiv endgültig. Daher sollten lokale Variablen entweder endgültig oder effektiv endgültig sein.
Wenn Sie nun bei Instanzfeldern auf ein Instanzfeld innerhalb des Lambda-Ausdrucks this
zugreifen, this
hängt der Compiler einen an diesen Variablenzugriff an (falls Sie dies nicht explizit getan haben). Da dies effektiv endgültig ist, ist der Compiler sicher, dass der Lambda-Ausdruckskörper dies tut Halten Sie immer die neueste Kopie der Variablen bereit (bitte beachten Sie, dass Multithreading derzeit für diese Diskussion nicht verfügbar ist). In Instanzfeldern kann der Compiler also feststellen, dass der Lambda-Body über die neueste Kopie der Instanzvariablen verfügt, sodass Instanzvariablen nicht endgültig oder effektiv endgültig sein müssen. Der folgende Screenshot zeigt eine Oracle-Folie:
Beachten Sie außerdem, dass beim Zugriff auf ein Instanzfeld im Lambda-Ausdruck, das in einer Umgebung mit mehreren Threads ausgeführt wird, möglicherweise Probleme auftreten können.
Anscheinend fragen Sie nach Variablen, auf die Sie von einem Lambda-Körper verweisen können.
Aus dem JLS §15.27.2
Alle lokalen Variablen, formalen Parameter oder Ausnahmeparameter, die in einem Lambda-Ausdruck verwendet, aber nicht deklariert werden, müssen entweder als endgültig oder als endgültig deklariert werden (§4.12.4). Andernfalls tritt ein Kompilierungsfehler auf, wenn die Verwendung versucht wird.
Sie müssen also keine Variablen deklarieren final
, sondern nur sicherstellen, dass sie "effektiv endgültig" sind. Dies ist die gleiche Regel wie für anonyme Klassen.
final
Einschränkung.
Innerhalb von Lambda-Ausdrücken können Sie effektiv endgültige Variablen aus dem umgebenden Bereich verwenden. Bedeutet effektiv, dass es nicht obligatorisch ist, die Variable final zu deklarieren, aber stellen Sie sicher, dass Sie ihren Status nicht innerhalb der Lambda-Expression ändern.
Sie können dies auch in Closures verwenden. Wenn Sie "this" verwenden, bedeutet dies, dass das einschließende Objekt, nicht jedoch das Lambda selbst, da Closures anonyme Funktionen sind und ihnen keine Klasse zugeordnet ist.
Wenn Sie also ein Feld (sagen wir private Ganzzahl i;) aus der einschließenden Klasse verwenden, das nicht als endgültig und nicht effektiv endgültig deklariert ist, funktioniert es weiterhin, da der Compiler den Trick in Ihrem Namen ausführt und "this" (this.i) einfügt. .
private Integer i = 0;
public void process(){
Consumer<Integer> c = (i)-> System.out.println(++this.i);
c.accept(i);
}
Hier ist ein Codebeispiel, da ich dies auch nicht erwartet hatte, erwartete ich, dass ich nichts außerhalb meines Lambda ändern konnte
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: Odd = true
JA, Sie können die Mitgliedsvariablen der Instanz ändern, aber Sie können die Instanz selbst NICHT wie beim Umgang mit Variablen ändern .
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 Einschränkung der lokalen Variablen betrifft die Gültigkeit von Daten und Berechnungen
Wenn das vom zweiten Thread bewertete Lambda die Fähigkeit erhalten würde, lokale Variablen zu mutieren. Selbst die Fähigkeit, den Wert veränderbarer lokaler Variablen aus einem anderen Thread zu lesen, würde die Notwendigkeit einer Synchronisation oder der Verwendung von flüchtigen Bestandteilen einführen , um das Lesen veralteter Daten zu vermeiden.
Aber wie wir wissen, ist der Hauptzweck der Lambdas
Unter den verschiedenen Gründen dafür ist der dringlichste für die Java-Plattform, dass sie es einfacher machen, die Verarbeitung von Sammlungen auf mehrere Threads zu verteilen .
Im Gegensatz zu lokalen Variablen kann die lokale Instanz mutiert werden, da sie global gemeinsam genutzt wird . Wir können dies besser über den Heap- und Stack-Unterschied verstehen :
Wenn ein Objekt erstellt wird, wird es immer im Heap-Bereich gespeichert, und der Stapelspeicher enthält den Verweis darauf. Der Stapelspeicher enthält nur lokale primitive Variablen und Referenzvariablen für Objekte im Heap-Bereich.
Zusammenfassend gibt es zwei Punkte, die meiner Meinung nach wirklich wichtig sind:
Es ist wirklich schwierig, die Instanz effektiv endgültig zu machen , was eine Menge sinnloser Belastungen verursachen kann (stellen Sie sich nur die tief verschachtelte Klasse vor).
Die Instanz selbst wird bereits global gemeinsam genutzt, und Lambda kann auch von Threads gemeinsam genutzt werden, sodass sie ordnungsgemäß zusammenarbeiten können, da wir wissen, dass wir mit der Mutation umgehen und diese Mutation weitergeben möchten.
Der Gleichgewichtspunkt hier ist klar: Wenn Sie wissen, was Sie tun, können Sie es leicht tun, aber wenn nicht , hilft die Standardeinschränkung , heimtückische Fehler zu vermeiden .
PS Wenn die Synchronisation für die Instanzmutation erforderlich ist , können Sie die Stream-Reduktionsmethoden direkt verwenden. Wenn bei der Instanzmutation ein Abhängigkeitsproblem auftritt , können Sie weiterhin thenApply
oder thenCompose
in Function while mapping
oder ähnliche Methoden verwenden.
Erstens gibt es einen wesentlichen Unterschied darin, wie lokale und Instanzvariablen hinter den Kulissen implementiert werden. Instanzvariablen werden im Heap gespeichert, während lokale Variablen im Stapel gespeichert werden. Wenn das Lambda direkt auf die lokale Variable zugreifen kann und das Lambda in einem Thread verwendet wird, kann der Thread, der das Lambda verwendet, versuchen, auf die Variable zuzugreifen, nachdem der Thread, der die Variable zugewiesen hat, die Zuordnung aufgehoben hat.
Kurz gesagt: Um sicherzustellen, dass ein anderer Thread den ursprünglichen Wert nicht überschreibt, ist es besser, Zugriff auf die Kopiervariable zu gewähren, als auf den ursprünglichen.