Lambdas: Lokale Variablen benötigen final, Instanzvariablen nicht

73

In einem Lambda müssen lokale Variablen endgültig sein, Instanzvariablen jedoch nicht. Warum so?

Gerard
quelle
Lassen Sie uns wissen, dass zumindest mit der neuesten Version des Compilers Java 1.8 lokale Variablen nur effektiv endgültig sein müssen, damit sie nicht per se als endgültig deklariert werden müssen, sondern nicht geändert werden können.
Valentin Ruano
Nachdem ich alle Antworten hier gelesen habe, denke ich immer noch, dass es sich nur um eine vom Compiler erzwungene Regel handelt, mit der Programmiererfehler minimiert werden sollen. Das heißt, es gibt keinen technischen Grund, warum veränderbare lokale Variablen nicht erfasst werden können oder warum erfasste lokale Variablen erfasst werden können. ' im Übrigen nicht mutiert sein. Dieser Punkt wird durch die Tatsache unterstützt, dass diese Regel mithilfe eines Objekt-Wrappers leicht umgangen werden kann (sodass die Objektreferenz effektiv endgültig ist, nicht jedoch das Objekt selbst). Eine andere Möglichkeit besteht darin, ein Array zu erstellen, d Integer[] count = {new Integer(5)}. H. Siehe auch stackoverflow.com/a/50457016/7154924 .
flow2k
@ McDowell, Lambdas sind nicht nur Syntaxzucker für anonyme Klassen, sondern ein völlig anderes Konstrukt.
Pacerier

Antworten:

60

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.

Adam Adamaszek
quelle
Richtig, dann muss der anonyme Klassenkonstruktor die Instanzvariable nicht kopieren, da er nur darauf verweisen kann. Gute Erklärung!
Gerard
24

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.

Kein Fehler
quelle
16

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,

sedooe
quelle
1
Ich denke wirklich, dass es in diesem Punkt einige Probleme in Java 8 in Aktion gibt. Wenn sich die lokale Variable hier auf die Variablen bezieht, die in der Methode erstellt wurden, auf die jedoch die Lambdas zugreifen, und der Multithread von erreicht wird 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 .
Gehört
Der erste Punkt ist also eigentlich nicht richtig, es gibt keine sogenannte Kopie , sie wird von Threads in parallelStream geteilt. Und das Teilen veränderlicher Variablen zwischen Threads ist genauso gefährlich wie der zweite Punkt . Aus diesem Grund verhindern wir dies und führen in Stream integrierte Methoden ein, um diese Fälle zu behandeln.
Gehört
14

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.

newacct
quelle
Schöne Erklärung für diese Frage
Sritam Jagadev
12

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 thiszugreifen, thishä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:

Geben Sie hier die Bildbeschreibung ein

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.

Hagrawal
quelle
2
Würde es Ihnen etwas ausmachen, die Quelle der Oracle-Folie anzugeben?
flow2k
@hagrawal Könnten Sie bitte Ihre endgültige Aussage bezüglich der Multithread-Umgebung ausarbeiten? Ist es in Bezug auf den tatsächlichen Wert der Mitgliedsvariablen zu jeder Zeit, da viele Threads gleichzeitig ausgeführt werden, so dass sie die Instanzvariable überschreiben können. Auch wenn ich die Mitgliedsvariablen richtig synchronisiere, bleibt auch das Problem bestehen?
Yug Singh
1
beste Antwort auf die Frage, denke ich;)
Supun Wijerathne
9

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.

Boris die Spinne
quelle
3
Ja, aber Instanzvariablen können in einem Lambda referenziert und zugewiesen werden, was für mich überraschend ist. Nur lokale Variablen haben die finalEinschränkung.
Gerard
@Gerard Da eine Instanzvariable den Gültigkeitsbereich der gesamten Klasse hat. Dies ist genau die gleiche Logik wie für eine anonyme Klasse - es gibt viele Tutorials, die die Logik erklären.
Boris die Spinne
6

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);
}
Nasioman
quelle
5

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

verloren
quelle
4

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:

  1. 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).

  2. 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 thenApplyoder thenComposein Function while mappingoder ähnliche Methoden verwenden.

Gehört
quelle
0

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.

Sambhav
quelle