2014-07-14 4 views
9

Ich habe den folgenden Code bekommt, die etwas von einer realen Implementierung abstrahiert ich in einem Java-Programm hatte:Warum kann ich in diesem Fall die Variable nicht innerhalb eines Lambda referenzieren?

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); 
String line; 
while ((line = bufferedReader.readLine()) != null) { 
    String lineReference = line; 
    runLater(() -> consumeString(lineReference)); 
} 

Hier muß ich eine Referenzkopie für den Lambda-Ausdruck verwenden, wenn ich versuche, line zu verwenden ich bekomme:

Lokale Variablen von einem Lambda-Ausdruck verwiesen wird, müssen endgültig sein oder effektiv endgültige

Es scheint eher umständlich zu mir, wie alle tun ich fixiere es eine neue Referenz auf das Objekt, das kann der Compiler auch selbst herausfinden.

Also würde ich sagen line ist effektiv endgültig hier, da es nur die Zuweisung in der Schleife und nirgendwo sonst erhält.

Könnte jemand mehr Licht in dieses Thema bringen und erklären, warum es genau hier gebraucht wird und warum das Kompilieren es nicht beheben kann?

+2

Warum verwenden Sie nicht [BufferedReader :: Linien] (http://docs.oracle.com/javase/8/docs/api/java/io/BufferedReader.html#lines--)? – nosid

+0

@nosid Funktioniert es bei einem Stream, der offen gelassen werden kann? – skiwi

+2

"final" oder "effektiv final" bedeutet, dass der Wert einmal zugewiesen und danach nie mehr geändert wird. 'line' wird mehrfach vergeben (einmal für jedes Mal, wenn eine neue Zeile gelesen wird). Sie müssen eine falsche Vorstellung davon haben, was "effektiv final" bedeutet - es geht nicht darum, wie viele * Plätze * im Quellcode zugewiesen sind, sondern darum, ob es während der Ausführung des Programms neu zugewiesen werden könnte. – ajb

Antwort

20

Also ich würde sagen line ist effektiv letzten hier, da es nur die Zuordnung in der Schleife und nirgendwo sonst bekommt.

Nein, es ist nicht endgültig, weil es während der Lebensdauer der Variablen einen neuen Wert für jede Schleifeniteration erhält. Dies ist das komplette Gegenteil von Finale.

erhalte ich: ‚Lokale Variablen von einem Lambda-Ausdruck verwiesen wird, müssen endgültig sein oder effektiv endgültig‘. Es erscheint mir ziemlich peinlich.

Betrachten Sie dies: Sie übergeben das Lambda an runLater(...). Wenn das Lambda schließlich ausgeführt wird, welchen Wert von line sollte es verwenden? Der Wert, den es hatte, als das Lambda erstellt wurde, oder der Wert, den es hatte, als das Lambda ausgeführt wurde?

Die Regel ist, dass Lambdas (scheinbar) den aktuellen Wert zum Zeitpunkt der Lambda-Ausführung verwenden. Sie erstellen (scheinbar) keine Kopie der Variablen. Wie wird diese Regel in der Praxis umgesetzt?

  • Wenn line ein statisches Feld ist, ist es einfach, weil es kein Staat für das Lambda zu erfassen. Das Lambda kann jederzeit den aktuellen Wert des Feldes lesen, genau wie jeder andere Code auch.

  • Wenn line ein Instanzfeld ist, ist das auch ziemlich einfach. Das Lambda kann den Verweis auf das Objekt in einem privaten versteckten Feld in jedem Lambda-Objekt erfassen und über dieses Feld auf das Feld line zugreifen.

  • Wenn line eine lokale Variable in einer Methode ist (wie in Ihrem Beispiel ist), ist dies plötzlich nicht einfach. Auf einer Implementierungsebene ist der Lambda-Ausdruck is in a completely different method, und es gibt keinen einfachen Weg für externen Code, den Zugriff auf die Variable zu teilen, die nur innerhalb der einen Methode existiert.

Um den Zugriff auf die lokale Variable zu aktivieren, würde der Compiler die Variable in einem verborgenen, veränderbare Halterobjekt (wie etwa ein 1-Element-Array), so dass der Halter Objekt sowohl von der referenziert werden zu dem Kasten konnte umschließende Methode und das Lambda, geben ihnen beide Zugriff auf die Variable innerhalb.

Obwohl diese Lösung technisch funktionieren würde, wäre das Verhalten, das sie erreicht, für ein Bündel von Gründen unerwünscht. Das Zuweisen des Halterobjekts würde lokalen Variablen ein unnatürliches Leistungsmerkmal geben, das beim Lesen des Codes nicht offensichtlich wäre. (Das bloße Definieren eines Lambdas, das eine lokale Variable verwendet, würde die Variable in der gesamten Methode langsamer machen.) Schlimmer noch, es würde subtile Race-Bedingungen in ansonsten einfachen Code einführen, abhängig davon, wann das Lambda ausgeführt wird. In Ihrem Beispiel könnte zu dem Zeitpunkt, zu dem das Lambda ausgeführt wird, eine beliebige Anzahl von Schleifeniterationen aufgetreten sein, oder die Methode könnte zurückgegeben worden sein, so dass die line Variable einen beliebigen Wert oder keinen definierten Wert haben könnte und fast sicher nicht den Wert Sie hätte gewollt. Also in der Praxis würden Sie immer noch brauchen die separate, unveränderliche lineReference Variable! Der einzige Unterschied ist, dass der Compiler das nicht benötigt, damit Sie kaputten Code schreiben können. Da das Lambda schließlich auf einem anderen Thread ausgeführt werden könnte, würde dies auch zu einer subtilen Parallelitäts- und Threadsichtbarkeitskomplexität für lokale Variablen führen, was erfordern würde, dass die Sprache den Modifizierer auf lokale Variablen und andere Störungen einstellt.

So für das Lambda zu sehen, die aktuellen sich ändernden Werte der lokalen Variablen würde eine Menge Aufwand (und keine Vorteile seit you can do the mutable holder trick manually, wenn Sie jemals brauchen). Stattdessen sagt die Sprache Nein zu der ganzen Kerfufle, indem sie einfach verlangt, dass die Variable final (oder effektiv endgültig) ist. Auf diese Weise kann das Lambda den Wert der lokalen Variablen zur Lambda-Erzeugungszeit erfassen, und es muss sich nicht darum kümmern, Änderungen zu erkennen, weil es weiß, dass es keine geben kann.

Das ist etwas, der Compiler auch selbst herausfinden konnte

Es war es herauszufinden, weshalb sie es nicht zulässt. Die lineReference Variable ist von keinerlei Vorteil für den Compiler, der den aktuellen Wert von line für Verwendung in dem Lambda bei jeder Erstellung des Lambda-Objekts problemlos erfassen konnte. Da das Lambda jedoch keine Änderungen an der Variablen erkennen würde (was aus den oben erläuterten Gründen nicht praktikabel und unerwünscht wäre), wäre der feine Unterschied zwischen der Erfassung von Feldern und der Erfassung von Ortsansässigen verwirrend. Die "final or effective final" -Regel kommt dem Programmierer zugute: Sie verhindert, dass Sie sich wundern, warum Änderungen an einer Variable nicht innerhalb eines Lambda erscheinen, indem Sie sie daran hindern, sie überhaupt zu ändern.Hier ist ein Beispiel dafür, was ohne diese Regel passieren würde:

String field = "A"; 
void foo() { 
    String local = "A"; 
    Runnable r =() -> System.out.println(field + local); 
    field = "B"; 
    local = "B"; 
    r.run(); // output: "BA" 
} 

Das Verwirrung geht weg, wenn alle lokalen Variablen innerhalb der Lambda-referenziert sind (effektiv) endgültig.

In Ihrem Code ist lineReference effektiv endgültig. Sein Wert wird während seiner Lebensdauer genau einmal zugewiesen, bevor es am Ende jeder Schleife Iteration aus dem Geltungsbereich hinausgeht, weshalb Sie es im Lambda verwenden können.

Es gibt eine alternative Anordnung der Schleife möglich durch line innerhalb der Schleife deklarieren:

for (;;) { 
    String line = bufferedReader.readLine(); 
    if (line == null) break; 
    runLater(() -> consumeString(line)); 
} 

Dies erlaubt ist, weil line jetzt außerhalb des Gültigkeitsbereichs am Ende jeder Schleife Iteration geht. Jede Iteration hat effektiv eine frische Variable, die genau einmal zugewiesen ist. (Allerdings ist die Variable auf einem niedrigen Niveau immer noch im selben CPU-Register gespeichert, also ist es nicht so, dass sie wiederholt "erzeugt" und "zerstört" werden muss. Was ich meine, ist, dass es keine zusätzlichen Kosten für die Deklaration von Variablen gibt eine Schleife wie diese, so ist es in Ordnung.)


Hinweis: All dies ist nicht einzigartig für lambdas. Sie gilt auch für alle lexikalisch deklarierten Klassen innerhalb der Methode, von denen lambdas die Regeln geerbt hat.

Anmerkung 2: Es könnte argumentiert werden, dass Lambdas einfacher wären, wenn sie der Regel folgen würden, die Werte von Variablen, die sie zur Zeit der Lambda-Erzeugung verwenden, immer zu erfassen. Dann würde es keinen Unterschied im Verhalten zwischen Feldern und Ortsansässigen geben, und keine Notwendigkeit für die "endgültige oder effektiv endgültige" Regel, weil es gut etabliert wäre, dass Lambdas keine Änderungen nach der Lambda-Erzeugungszeit sehen. Aber diese Regel hätte ihre eigenen Uglines. Als ein Beispiel würde für ein Instanzfeld x, auf das innerhalb eines Lambda zugegriffen wird, ein Unterschied zwischen dem Verhalten des Lesens x (Erfassungsendwert von x) und this.x (Erfassungsendwert von this, wobei sein Feld x sich ändern wird) vorliegen. Sprachdesign ist schwer.

+0

Nizza, +1. Dies steht in engem Zusammenhang mit dem Problem, dass nicht-mutate-captured-nonfinal-locals mit anonymen inneren Klassen behandelt werden kann. Der einzige Unterschied in Java 8 besteht darin, dass solche Variablen nicht als "final" deklariert werden müssen, wenn sie effektiv final sind. (Dies gilt sowohl für Lambdas als auch für AICs.) Als solche ist die "effektiv endgültige" Regel lediglich eine Bequemlichkeit, keine semantische Änderung. Mehr Geschichte in dieser Antwort von [Jon Skeet] (http://stackoverflow.com/a/4732617/1441122) und diese Antwort von [gnat] (http://stackoverflow.com/a/17736622/1441122). –

1

Wenn Sie line statt lineReference in dem Lambda-Ausdruck verwenden würden, würden Sie einen Lambda-Ausdruck zu Ihrer runLater Methode werden vorbei, die consumeString auf einem String bezeichnet von line ausführen würde.

Aber line ändert sich ständig, wenn Sie ihm neue Zeilen zuweisen. Wenn Sie schließlich die Methode der funktionalen Schnittstelle ausführen, die vom Lambda-Ausdruck zurückgegeben wird, wird nur der aktuelle Wert line abgerufen und im Aufruf von consumeString verwendet. An diesem Punkt wäre der Wert line nicht derselbe wie bei der Übergabe des Lambda-Ausdrucks an die runLater-Methode.

+0

Ich denke Sie haben 'line' und' lineReference' durcheinander gebracht. 'line' ist die einzige Variable, die sich ändert, wenn neue Zeilen zugewiesen werden. 'lineReference' ändert sich nicht während seiner Lebensdauer, da es innerhalb des Schleifenkörpers deklariert ist und jede Schleifeniteration eine logisch" neue "' lineReference'-Variable hat. – Boann

+0

@Boann Du hast Recht. Das habe ich vermisst. Vielen Dank! Habe es jetzt behoben, aber deine Antwort ist immer noch viel besser als meine. +1 – Eran

Verwandte Themen