Warum sind nur endgültige Variablen in anonymen Klassen zugänglich?

354
  1. akann hier nur endgültig sein. Warum? Wie kann ich neu zuweisen ain onClick()Verfahren ohne es als privat Mitglied zu halten?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
    
  2. Wie kann ich das zurückgeben, 5 * awenn es geklickt hat? Ich meine,

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                 int b = a*5;
                 return b; // but return type is void 
            }
        });
    }
    
Andrii Abramov
quelle
1
Ich glaube nicht, dass anonyme Java-Klassen die Art von Lambda-Schließung bieten, die Sie erwarten würden, aber jemand korrigiert mich bitte, wenn ich falsch
liege
4
Was versuchst du zu erreichen? Der Click-Handler kann ausgeführt werden, wenn "f" beendet ist.
Ivan Dubrov
@Lambert , wenn Sie ein in der onClick - Methode verwenden möchten , haben es endgültig @Ivan Wie kann f () Methode verhält sich wie onClick () -Methode return int , wenn darauf geklickt werden
2
Das meine ich damit - es unterstützt keinen vollständigen Abschluss, da es keinen Zugriff auf nicht endgültige Variablen ermöglicht.
user541686
4
Hinweis:
Ab

Antworten:

489

Wie in den Kommentaren erwähnt, ist ein Teil davon in Java 8 irrelevant, wo finaldies implizit sein kann. In einer anonymen inneren Klasse oder einem Lambda-Ausdruck kann jedoch nur eine effektiv endgültige Variable verwendet werden.


Dies liegt im Wesentlichen an der Art und Weise, wie Java Schließungen verwaltet .

Wenn Sie eine Instanz einer anonymen inneren Klasse erstellen, werden die Werte aller Variablen, die in dieser Klasse verwendet werden, über den automatisch generierten Konstruktor kopiert. Dies vermeidet, dass der Compiler verschiedene zusätzliche Typen automatisch generieren muss, um den logischen Status der "lokalen Variablen" zu speichern, wie dies beispielsweise der C # -Compiler tut ... (Wenn C # eine Variable in einer anonymen Funktion erfasst, erfasst es die Variable - die Das Schließen kann die Variable auf eine Weise aktualisieren, die vom Hauptteil der Methode gesehen wird, und umgekehrt.)

Da der Wert in die Instanz der anonymen inneren Klasse kopiert wurde, würde es seltsam aussehen, wenn die Variable durch den Rest der Methode geändert werden könnte - Sie könnten Code haben, der anscheinend mit einer veralteten Variablen funktioniert ( denn genau das würde passieren ... Sie würden mit einer Kopie arbeiten, die zu einem anderen Zeitpunkt aufgenommen wurde. Wenn Sie Änderungen innerhalb der anonymen inneren Klasse vornehmen könnten, könnten Entwickler erwarten, dass diese Änderungen im Hauptteil der einschließenden Methode sichtbar sind.

Wenn Sie die Variable final machen, werden alle diese Möglichkeiten entfernt. Da der Wert überhaupt nicht geändert werden kann, müssen Sie sich keine Gedanken darüber machen, ob solche Änderungen sichtbar sind. Die einzige Möglichkeit, der Methode und der anonymen inneren Klasse zu erlauben, die Änderungen des anderen zu sehen, besteht darin, einen veränderlichen Typ einer Beschreibung zu verwenden. Dies könnte die einschließende Klasse selbst sein, ein Array, ein veränderbarer Wrapper-Typ ... so etwas. Im Grunde ist es ein bisschen wie die Kommunikation zwischen einer Methode und einer anderen: Änderungen, die an den Parametern einer Methode vorgenommen wurden, werden vom Aufrufer nicht gesehen, aber Änderungen an den Objekten, auf die die Parameter verweisen, werden angezeigt.

Wenn Sie an einem detaillierteren Vergleich zwischen Java- und C # -Verschlüssen interessiert sind, habe ich einen Artikel, der weiter darauf eingeht. Ich wollte mich in dieser Antwort auf die Java-Seite konzentrieren :)

Jon Skeet
quelle
4
@ Ivan: Im Grunde wie C #. Es ist jedoch mit einem angemessenen Grad an Komplexität verbunden, wenn Sie dieselbe Art von Funktionalität wie C # wünschen, bei der Variablen aus verschiedenen Bereichen unterschiedlich oft "instanziiert" werden können.
Jon Skeet
11
Dies galt alles für Java 7, denken Sie daran, dass mit Java 8 Schließungen eingeführt wurden und es nun tatsächlich möglich ist, von seiner inneren Klasse aus auf ein nicht endgültiges Feld einer Klasse zuzugreifen.
Mathias Bader
22
@ MathiasBader: Wirklich? Ich dachte, es ist immer noch im Wesentlichen der gleiche Mechanismus, der Compiler ist jetzt gerade klug genug, um daraus zu schließen final(aber er muss noch effektiv endgültig sein).
Thilo
3
@Mathias Bader: Sie können immer auf nicht endgültige Felder zugreifen , die nicht mit lokalen Variablen zu verwechseln sind, die endgültig sein müssen und dennoch effektiv endgültig sein müssen, damit Java 8 die Semantik nicht ändert.
Holger
41

Es gibt einen Trick, mit dem anonyme Klassen Daten im äußeren Bereich aktualisieren können.

private void f(Button b, final int a) {
    final int[] res = new int[1];
    b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            res[0] = a * 5;
        }
    });

    // But at this point handler is most likely not executed yet!
    // How should we now res[0] is ready?
}

Dieser Trick ist jedoch aufgrund der Synchronisationsprobleme nicht sehr gut. Wenn der Handler später aufgerufen wird, müssen Sie 1) den Zugriff auf res synchronisieren, wenn der Handler vom anderen Thread aufgerufen wurde. 2) eine Art Flag oder Anzeige haben, dass res aktualisiert wurde

Dieser Trick funktioniert jedoch einwandfrei, wenn eine anonyme Klasse sofort im selben Thread aufgerufen wird. Mögen:

// ...

final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);

// ...
Ivan Dubrov
quelle
2
Danke für deine Antwort. Ich weiß das alles und meine Lösung ist besser als diese. meine frage ist "warum nur endgültig"?
6
Die Antwort ist dann, wie sie implementiert werden :)
Ivan Dubrov
1
Vielen Dank. Ich hatte den obigen Trick alleine benutzt. Ich war mir nicht sicher, ob es eine gute Idee ist. Wenn Java dies nicht zulässt, gibt es möglicherweise einen guten Grund. Ihre Antwort verdeutlicht, dass mein List.forEachCode sicher ist.
RuntimeException
Lesen Sie stackoverflow.com/q/12830611/2073130, um eine gute Diskussion der Gründe für "warum nur endgültig" zu erhalten.
lcn
Es gibt mehrere Problemumgehungen. Meins ist: final int resf = res; Anfangs habe ich den Array-Ansatz verwendet, aber ich finde, dass er eine zu umständliche Syntax hat. AtomicReference ist möglicherweise etwas langsamer (weist ein Objekt zu).
Zakmck
17

Eine anonyme Klasse ist eine innere Klasse und die strenge Regel gilt für innere Klassen (JLS 8.1.3) :

Alle lokalen Variablen, formalen Methodenparameter oder Ausnahmebehandlungsparameter, die in einer inneren Klasse verwendet, aber nicht deklariert werden, müssen als endgültig deklariert werden . Jede lokale Variable, die in einer inneren Klasse verwendet, aber nicht deklariert wird, muss definitiv vor dem Hauptteil der inneren Klasse zugewiesen werden .

Ich habe noch keinen Grund oder eine Erklärung für die jls oder jvms gefunden, aber wir wissen, dass der Compiler für jede innere Klasse eine separate Klassendatei erstellt und sicherstellen muss, dass die in dieser Klassendatei deklarierten Methoden ( auf Bytecode-Ebene) mindestens Zugriff auf die Werte lokaler Variablen haben.

( Jon hat die vollständige Antwort - ich halte diese ungelöscht, weil man an der JLS-Regel interessiert sein könnte)

Andreas Dolk
quelle
11

Sie können eine Variable auf Klassenebene erstellen, um den zurückgegebenen Wert abzurufen. ich meine

class A {
    int k = 0;
    private void f(Button b, int a){
        b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            k = a * 5;
        }
    });
}

Jetzt können Sie den Wert von K erhalten und ihn verwenden, wo Sie möchten.

Die Antwort auf Ihr Warum lautet:

Eine lokale Instanz der inneren Klasse ist an die Hauptklasse gebunden und kann auf die endgültigen lokalen Variablen ihrer enthaltenen Methode zugreifen. Wenn die Instanz ein endgültiges lokales Element ihrer enthaltenen Methode verwendet, behält die Variable den Wert bei, den sie zum Zeitpunkt der Erstellung der Instanz hatte, auch wenn die Variable den Gültigkeitsbereich verlassen hat (dies ist effektiv Javas grobe, begrenzte Version von Schließungen).

Da eine lokale innere Klasse weder Mitglied einer Klasse noch eines Pakets ist, wird sie nicht mit einer Zugriffsebene deklariert. (Stellen Sie jedoch klar, dass die eigenen Mitglieder Zugriffsebenen wie in einer normalen Klasse haben.)

Ashfak Balooch
quelle
Ich erwähnte das "ohne es als privates Mitglied zu behalten"
6

Nun, in Java kann eine Variable nicht nur als Parameter endgültig sein, sondern auch als Feld auf Klassenebene

public class Test
{
 public final int a = 3;

oder als lokale Variable, wie

public static void main(String[] args)
{
 final int a = 3;

Wenn Sie von einer anonymen Klasse aus auf eine Variable zugreifen und diese ändern möchten, möchten Sie die Variable möglicherweise zu einer Variablen auf Klassenebene in der umschließenden Klasse machen.

public class Test
{
 public int a;
 public void doSomething()
 {
  Runnable runnable =
   new Runnable()
   {
    public void run()
    {
     System.out.println(a);
     a = a+1;
    }
   };
 }
}

Sie können eine Variable nicht als endgültig festlegen und ihr einen neuen Wert geben. finalbedeutet genau das: Der Wert ist unveränderlich und endgültig.

Und da es endgültig ist, kann Java es sicher in lokale anonyme Klassen kopieren . Sie erhalten keinen Verweis auf das int (insbesondere, da Sie in Java keine Verweise auf Grundelemente wie int haben können, sondern nur Verweise auf Objekte ).

Es kopiert einfach den Wert von a in ein implizites int, das in Ihrer anonymen Klasse als a bezeichnet wird.

Zach L.
quelle
3
Ich verbinde "Variable auf Klassenebene" mit static. Vielleicht ist es klarer, wenn Sie stattdessen "Instanzvariable" verwenden.
Eljenso
1
Nun, ich habe Klassenebene verwendet, da die Technik sowohl mit Instanz- als auch mit statischen Variablen funktionieren würde.
Zach L
Wir wissen bereits, dass das Finale zugänglich ist, aber wir wollen wissen warum? Können Sie bitte eine weitere Erklärung hinzufügen, warum?
Saurabh Oza
6

Der Grund, warum der Zugriff nur auf die lokalen endgültigen Variablen beschränkt wurde, besteht darin, dass, wenn alle lokalen Variablen zugänglich gemacht würden, sie zuerst in einen separaten Abschnitt kopiert werden müssten, in dem innere Klassen Zugriff auf sie haben und mehrere Kopien von verwalten könnten veränderbare lokale Variablen können zu inkonsistenten Daten führen. Während endgültige Variablen unveränderlich sind und daher eine beliebige Anzahl von Kopien keine Auswirkungen auf die Datenkonsistenz hat.

Swapnil Gangrade
quelle
So wird es nicht in Sprachen wie C # implementiert, die diese Funktion unterstützen. Tatsächlich ändert der Compiler die Variable von einer lokalen Variablen in eine Instanzvariable oder erstellt eine zusätzliche Datenstruktur für diese Variablen, die den Umfang der äußeren Klasse überleben kann. Es gibt jedoch keine "Mehrfachkopien lokaler Variablen"
Mike76
Mike76 Ich habe mir die Implementierung von C # nicht angesehen, aber Scala macht das zweite, was Sie erwähnt haben. Ich denke: Wenn ein Intinnerhalb eines Abschlusses neu zugewiesen wird, ändern Sie diese Variable in eine Instanz von IntRef(im Wesentlichen einen veränderlichen IntegerWrapper). Jeder Variablenzugriff wird dann entsprechend umgeschrieben.
Adowrath
3

Betrachten Sie das folgende Programm, um die Gründe für diese Einschränkung zu verstehen:

public class Program {

    interface Interface {
        public void printInteger();
    }
    static Interface interfaceInstance = null;

    static void initialize(int val) {
        class Impl implements Interface {
            @Override
            public void printInteger() {
                System.out.println(val);
            }
        }
        interfaceInstance = new Impl();
    }

    public static void main(String[] args) {
        initialize(12345);
        interfaceInstance.printInteger();
    }
}

Die interfaceInstance bleibt nach der Rückkehr der Initialisierungsmethode im Speicher , der Parameter val jedoch nicht. Die JVM kann nicht auf eine lokale Variable außerhalb ihres Gültigkeitsbereichs zugreifen. Daher führt Java den nachfolgenden Aufruf von printInteger aus , indem der Wert von val in ein implizites Feld mit demselben Namen in interfaceInstance kopiert wird . Die interfaceInstance soll den Wert des lokalen Parameters erfasst haben . Wenn der Parameter nicht endgültig (oder effektiv endgültig) wäre, könnte sich sein Wert ändern und nicht mehr mit dem erfassten Wert synchronisiert werden, was möglicherweise zu einem nicht intuitiven Verhalten führen könnte.

Benjamin Curtis Drake
quelle
2

Methoden innerhalb einer anonomischen inneren Klasse können gut aufgerufen werden, nachdem der Thread, der sie erzeugt hat, beendet wurde. In Ihrem Beispiel wird die innere Klasse im Ereignisversand-Thread aufgerufen und nicht im selben Thread wie der, der sie erstellt hat. Daher ist der Umfang der Variablen unterschiedlich. Um solche Probleme mit dem Variablenzuweisungsbereich zu schützen, müssen Sie sie für endgültig erklären.

S73417H
quelle
2

Wenn eine anonyme innere Klasse im Hauptteil einer Methode definiert ist, kann auf alle im Bereich dieser Methode als endgültig deklarierten Variablen innerhalb der inneren Klasse zugegriffen werden. Bei skalaren Werten kann sich der Wert der endgültigen Variablen nach der Zuweisung nicht mehr ändern. Bei Objektwerten kann sich die Referenz nicht ändern. Auf diese Weise kann der Java-Compiler den Wert der Variablen zur Laufzeit "erfassen" und eine Kopie als Feld in der inneren Klasse speichern. Sobald die äußere Methode beendet und ihr Stapelrahmen entfernt wurde, ist die ursprüngliche Variable verschwunden, aber die private Kopie der inneren Klasse bleibt im eigenen Speicher der Klasse erhalten.

( http://en.wikipedia.org/wiki/Final_%28Java%29 )

ola_star
quelle
1
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

        @Override
        public void onClick(ClickEvent event) {
            a[0] = a[0] * 5;

        }
    });
}
Bruce Zu
quelle
0

Da Jon die Implementierungsdetails beantwortet hat, wäre eine andere mögliche Antwort, dass die JVM das Schreiben von Datensätzen, die seine Aktivierung beendet haben, nicht verarbeiten möchte.

Stellen Sie sich den Anwendungsfall vor, in dem Ihre Lambdas nicht angewendet, sondern an einem Ort gespeichert und später ausgeführt werden.

Ich erinnere mich, dass Sie in Smalltalk ein illegales Geschäft eröffnen würden, wenn Sie solche Änderungen vornehmen.

Mathk
quelle
0

Versuchen Sie diesen Code,

Erstellen Sie eine Array-Liste, geben Sie einen Wert ein und geben Sie ihn zurück:

private ArrayList f(Button b, final int a)
{
    final ArrayList al = new ArrayList();
    b.addClickHandler(new ClickHandler() {

         @Override
        public void onClick(ClickEvent event) {
             int b = a*5;
             al.add(b);
        }
    });
    return al;
}
Wasim
quelle
OP fragt nach Gründen, warum etwas erforderlich ist. Daher sollten Sie darauf hinweisen, wie Ihr Code es anspricht
NitinSingh
0

Die anonyme Java-Klasse ist dem Schließen von Javascript sehr ähnlich, aber Java implementiert dies auf andere Weise. (Überprüfen Sie Andersens Antwort)

Um den Java-Entwickler nicht mit dem seltsamen Verhalten zu verwechseln, das bei Personen mit Javascript-Hintergrund auftreten kann. Ich denke, deshalb zwingen sie uns final, dies zu verwenden . Dies ist nicht die JVM-Einschränkung.

Schauen wir uns das folgende Javascript-Beispiel an:

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

In Javascript ist der counterWert 100, da es countervon Anfang bis Ende nur eine Variable gibt .

Wenn dies in Java nicht der Fall ist final, wird es ausgedruckt 0, da der 0Wert während der Erstellung des inneren Objekts in die verborgenen Eigenschaften des Objekts der inneren Klasse kopiert wird. (Hier gibt es zwei ganzzahlige Variablen, eine in der lokalen Methode und eine in den versteckten Eigenschaften der inneren Klasse.)

Änderungen nach der Erstellung des inneren Objekts (wie Zeile 1) wirken sich also nicht auf das innere Objekt aus. Dies führt zu Verwechslungen zwischen zwei unterschiedlichen Ergebnissen und Verhaltensweisen (zwischen Java und Javascript).

Ich glaube, aus diesem Grund beschließt Java, die endgültige Festlegung zu erzwingen, sodass die Daten von Anfang bis Ende "konsistent" sind.

GMsoF
quelle
0

Java letzte Variable innerhalb einer inneren Klasse

innere Klasse kann nur verwenden

  1. Referenz aus der äußeren Klasse
  2. endgültige lokale Variablen außerhalb des Gültigkeitsbereichs, die ein Referenztyp sind (z. B. Object...)
  3. Wert (primitiver) (zB int...) Typ kann gewickelt durch einen endgültigen Referenztyp. IntelliJ IDEAkann Ihnen helfen, es in ein Elementarray umzuwandeln

Wenn ein non static nested( inner class) [Info] vom Compiler generiert wird - eine neue Klasse - <OuterClass>$<InnerClass>.classwird erstellt und gebundene Parameter an den Konstruktor [Lokale Variable auf Stapel] übergeben . Es ist ähnlich wie bei der Schließung

Die endgültige Variable ist eine Variable, die nicht neu zugewiesen werden kann. Die endgültige Referenzvariable kann weiterhin durch Ändern eines Status geändert werden

Es war möglich, dass es komisch wäre, weil man als Programmierer so etwas machen könnte

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}
yoAlex5
quelle
-2

Vielleicht gibt dir dieser Trick eine Idee

Boolean var= new anonymousClass(){
    private String myVar; //String for example
    @Overriden public Boolean method(int i){
          //use myVar and i
    }
    public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);
Launisch
quelle