Perché solo le variabili finali sono accessibili in classe anonima?


355
  1. apuò essere solo definitivo qui. Perché? Come posso riassegnare anel onClick()metodo senza tenerlo come membro privato?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
  2. Come posso restituire il 5 * aclic? Intendo,

    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 
            }
        });
    }

1
Non penso che le classi anonime Java forniscano il tipo di chiusura lambda che ti aspetteresti, ma qualcuno per favore mi corregga se sbaglio ...
user541686

4
Cosa stai cercando di ottenere? Il gestore di clic potrebbe essere eseguito al termine di "f".
Ivan Dubrov,

@Lambert se si desidera utilizzare un metodo onClick, deve essere finale @Ivan come può il metodo f () comportarsi come il metodo onClick () restituire int quando si fa clic

2
Questo è ciò che intendo: non supporta la chiusura completa perché non consente l'accesso a variabili non finali.
user541686

4
Nota: a partire da Java 8, la tua variabile deve solo essere effettivamente definitiva
Peter Lawrey,

Risposte:


489

Come notato nei commenti, parte di questo diventa irrilevante in Java 8, dove finalpuò essere implicito. Tuttavia, solo una variabile effettivamente efficace può essere utilizzata in una classe interna anonima o in un'espressione lambda.


Fondamentalmente è dovuto al modo in cui Java gestisce le chiusure .

Quando si crea un'istanza di una classe interna anonima, tutte le variabili utilizzate all'interno di quella classe vengono copiate i loro valori tramite il costruttore generato automaticamente. Questo evita che il compilatore debba generare automaticamente vari tipi extra per contenere lo stato logico delle "variabili locali", come ad esempio il compilatore C # fa ... (Quando C # acquisisce una variabile in una funzione anonima, cattura davvero la variabile - il la chiusura può aggiornare la variabile in un modo visto dal corpo principale del metodo e viceversa.)

Poiché il valore è stato copiato nell'istanza della classe interna anonima, sembrerebbe strano se la variabile potesse essere modificata con il resto del metodo: potresti avere un codice che sembra funzionare con una variabile non aggiornata ( perché è effettivamente quello che succederebbe ... lavoreresti con una copia presa in un altro momento). Allo stesso modo, se è possibile apportare modifiche all'interno della classe interna anonima, gli sviluppatori potrebbero aspettarsi che tali modifiche siano visibili all'interno del corpo del metodo allegato.

Rendere la variabile finale rimuove tutte queste possibilità - poiché il valore non può essere modificato affatto, non devi preoccuparti se tali modifiche saranno visibili. L'unico modo per consentire al metodo e alla classe interna anonima di vedere le modifiche reciproche è utilizzare un tipo mutabile di qualche descrizione. Questa potrebbe essere la classe che racchiude in sé, un array, un tipo di involucro mutevole ... qualcosa del genere. Fondamentalmente è un po 'come comunicare tra un metodo e un altro: le modifiche apportate ai parametri di un metodo non vengono visualizzate dal suo chiamante, ma vengono visualizzate le modifiche apportate agli oggetti a cui fanno riferimento i parametri.

Se sei interessato a un confronto più dettagliato tra le chiusure Java e C #, ho un articolo che approfondisce ulteriormente. Volevo concentrarmi sul lato Java in questa risposta :)


4
@Ivan: come C #, in sostanza. Viene fornito con un discreto grado di complessità, se si desidera lo stesso tipo di funzionalità di C # in cui variabili di ambiti diversi possono essere "istanziati" diversi numeri di volte.
Jon Skeet,


11
Questo era tutto vero per Java 7, tenere presente che con Java 8 sono state introdotte chiusure e ora è davvero possibile accedere a un campo non finale di una classe dalla sua classe interna.
Mathias Bader,

22
@MathiasBader: Davvero? Ho pensato che fosse essenzialmente lo stesso meccanismo, il compilatore ora è abbastanza intelligente da inferire final(ma deve ancora essere efficacemente finale).
Thilo

3
@Mathias Bader: potresti sempre accedere ai campi non finali , che non devono essere confusi con le variabili locali , che dovevano essere definitive e devono comunque essere effettivamente definitive, quindi Java 8 non cambia la semantica.
Holger,

41

Esiste un trucco che consente alla classe anonima di aggiornare i dati nell'ambito esterno.

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?
}

Tuttavia, questo trucco non è molto buono a causa dei problemi di sincronizzazione. Se il gestore viene richiamato in un secondo momento, è necessario 1) sincronizzare l'accesso a res se il gestore è stato invocato dal diverso thread 2) è necessario disporre di una sorta di flag o indicazione che res è stato aggiornato

Questo trucco funziona bene, tuttavia, se la classe anonima viene richiamata immediatamente nello stesso thread. Piace:

// ...

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

// ...

2
grazie per la tua risposta. So tutto questo e la mia soluzione è migliore di così. la mia domanda è "perché solo finale"?

6
La risposta quindi è che sono così implementati :)
Ivan Dubrov,

1
Grazie. Avevo usato il trucco sopra per conto mio. Non ero sicuro che fosse una buona idea. Se Java non lo consente, potrebbe esserci una buona ragione. La tua risposta chiarisce che il mio List.forEachcodice è sicuro.
RuntimeException,

Leggi stackoverflow.com/q/12830611/2073130 per una buona discussione della logica alla base di "why only final".
lcn

ci sono diverse soluzioni alternative. Il mio è: final int resf = res; Inizialmente ho usato l'approccio array, ma trovo che abbia una sintassi troppo ingombrante. AtomicReference è forse un po 'più lento (alloca un oggetto).
zakmck,

17

Una classe anonima è una classe interna e la regola rigida si applica alle classi interne (JLS 8.1.3) :

Qualsiasi variabile locale, parametro del metodo formale o parametro del gestore delle eccezioni utilizzato ma non dichiarato in una classe interna deve essere dichiarato finale . Qualsiasi variabile locale, utilizzata ma non dichiarata in una classe interna, deve essere definitivamente assegnata al corpo della classe interna .

Non ho ancora trovato un motivo o una spiegazione su jls o jvms, ma sappiamo che il compilatore crea un file di classe separato per ogni classe interna e deve assicurarsi che i metodi dichiarati su questo file di classe ( a livello di codice byte) almeno avere accesso ai valori delle variabili locali.

( Jon ha la risposta completa - Tengo questo sfrenato perché uno potrebbe essere interessato alla regola JLS)


11

È possibile creare una variabile a livello di classe per ottenere il valore restituito. intendo

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;
        }
    });
}

ora puoi ottenere il valore di K e usarlo dove vuoi.

La risposta del tuo perché è:

Un'istanza di classe interna locale è collegata alla classe Main e può accedere alle variabili locali finali del suo metodo di contenimento. Quando l'istanza utilizza un locale finale del suo metodo di contenimento, la variabile conserva il valore che deteneva al momento della creazione dell'istanza, anche se la variabile è andata fuori dal campo di applicazione (si tratta effettivamente della versione grezza e limitata di chiusure di Java).

Poiché una classe interna locale non è né membro di una classe o pacchetto, non viene dichiarata con un livello di accesso. (Sii chiaro, tuttavia, che i suoi membri hanno livelli di accesso come in una classe normale.)


Ho detto che "senza tenerlo come membro privato"

6

Bene, in Java, una variabile può essere finale non solo come parametro, ma come campo a livello di classe, come

public class Test
{
 public final int a = 3;

o come una variabile locale, come

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

Se si desidera accedere e modificare una variabile da una classe anonima, è possibile rendere la variabile una variabile a livello di classe nella classe che la racchiude .

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

Non puoi avere una variabile come finale e darle un nuovo valore. finalsignifica proprio questo: il valore è immutabile e definitivo.

E poiché è definitivo, Java può tranquillamente copiarlo in classi anonime locali. Non stai ottenendo alcun riferimento all'int (soprattutto perché non puoi avere riferimenti a primitive come int in Java, solo riferimenti ad oggetti ).

Copia solo il valore di a in un int implicito chiamato a nella tua classe anonima.


3
Associo "variabile a livello di classe" a static. Forse è più chiaro se si utilizza invece "variabile di istanza".
eljenso,

1
bene, ho usato a livello di classe perché la tecnica avrebbe funzionato sia con istanze che con variabili statiche.
Zach L,

sappiamo già che i final sono accessibili ma vogliamo sapere perché? puoi per favore aggiungere qualche spiegazione in più sul perché lato?
Saurabh Oza,

6

Il motivo per cui l'accesso è stato limitato solo alle variabili finali locali è che se tutte le variabili locali fossero rese accessibili, avrebbero prima bisogno di essere copiate in una sezione separata in cui le classi interne possono accedervi e mantenere più copie di variabili locali mutabili possono portare a dati incoerenti. Considerando che le variabili finali sono immutabili e quindi qualsiasi numero di copie non avrà alcun impatto sulla coerenza dei dati.


Non è così che viene implementato in linguaggi come C # che supportano questa funzione. In effetti, il compilatore cambia la variabile da una variabile locale a una variabile di istanza oppure crea una struttura dati aggiuntiva per queste variabili che può sopravvivere all'ambito della classe esterna. Tuttavia, non ci sono "copie multiple di variabili locali"
Mike76

Mike76 Non ho dato un'occhiata all'implementazione di C #, ma Scala fa la seconda cosa che hai menzionato, penso: se Intsi sta riassegnando un IntRefelemento all'interno di una chiusura, cambiare quella variabile in un'istanza di (essenzialmente un Integerwrapper mutabile ). Ogni accesso variabile viene quindi riscritto di conseguenza.
Adowrath,

3

Per comprendere la logica di questa limitazione, considerare il seguente programma:

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();
    }
}

L'interfaceInstance rimane nella memoria dopo le inizializzazione metodo restituisce, ma il parametro val non lo fa. JVM non può accedere a una variabile locale al di fuori del suo ambito, quindi Java fa funzionare la successiva chiamata a printInteger copiando il valore di val in un campo implicito con lo stesso nome all'interno di interfaceInstance . Si dice che interfaceInstance abbia acquisito il valore del parametro locale. Se il parametro non fosse definitivo (o effettivamente definitivo) il suo valore potrebbe cambiare, diventando non sincronizzato con il valore acquisito, causando potenzialmente un comportamento non intuitivo.


2

I metodi all'interno di una classe interna anonima possono essere invocati anche dopo che il thread che l'ha generato è terminato. Nel tuo esempio, la classe interna verrà invocata sul thread di invio degli eventi e non nello stesso thread di quello che l'ha creata. Quindi, l'ambito delle variabili sarà diverso. Pertanto, per proteggere tali problemi relativi all'ambito delle assegnazioni variabili, è necessario dichiararli definitivi.


2

Quando una classe interna anonima viene definita all'interno del corpo di un metodo, tutte le variabili dichiarate finali nell'ambito di tale metodo sono accessibili dall'interno della classe interna. Per i valori scalari, una volta assegnato, il valore della variabile finale non può cambiare. Per i valori degli oggetti, il riferimento non può cambiare. Ciò consente al compilatore Java di "acquisire" il valore della variabile in fase di esecuzione e di archiviarne una copia come campo nella classe interna. Una volta terminato il metodo esterno e rimosso il frame dello stack, la variabile originale scompare ma la copia privata della classe interna persiste nella memoria stessa della classe.

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


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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

0

Dato che Jon ha la risposta ai dettagli dell'implementazione, un'altra possibile risposta sarebbe che la JVM non vuole gestire la scrittura nei record che hanno terminato la sua attivazione.

Considera il caso d'uso in cui il tuo lambdas invece di essere applicato, viene archiviato in qualche luogo ed eseguito in un secondo momento.

Ricordo che in Smalltalk verrebbe creato un negozio illegale quando si apportano tali modifiche.


0

Prova questo codice,

Crea un elenco di array e inserisci un valore al suo interno e restituiscilo:

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;
}

OP chiede ragioni per cui è necessario qualcosa. Quindi dovresti sottolineare come il tuo codice lo sta indirizzando
NitinSingh,

0

La classe anonima Java è molto simile alla chiusura di Javascript, ma Java la implementa in modo diverso. (controlla la risposta di Andersen)

Quindi, al fine di non confondere lo sviluppatore Java con lo strano comportamento che potrebbe verificarsi per coloro che provengono da Javascript background. Immagino sia per questo che ci costringono a usare final, questa non è la limitazione di JVM.

Diamo un'occhiata all'esempio Javascript di seguito:

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, il countervalore sarà 100, perché ce n'è solo unocounter variabile dall'inizio alla fine.

Ma in Java, se non c'è final, verrà stampato 0, perché mentre l'oggetto interno viene creato, il 0valore viene copiato nelle proprietà nascoste dell'oggetto di classe interno. (ci sono due variabili intere qui, una nel metodo locale, un'altra nelle proprietà nascoste della classe interna)

Pertanto, qualsiasi modifica dopo la creazione dell'oggetto interno (come la linea 1), non influirà sull'oggetto interno. Quindi creerà confusione tra due diversi risultati e comportamenti (tra Java e Javascript).

Credo che sia per questo motivo che Java decide di forzarlo a essere definitivo, quindi i dati sono "coerenti" dall'inizio alla fine.


0

Variabile finale Java all'interno di una classe interna

la classe interna può usare solo

  1. riferimento dalla classe esterna
  2. variabili locali finali fuori dal campo di applicazione che sono un tipo di riferimento (ad es. Object...)
  3. il tipo di valore (primitivo) (es. int...) può essere racchiuso da un tipo di riferimento finale. IntelliJ IDEApuò aiutarti a convertirlo in un array di elementi

Quando un non static nested( inner class) [Informazioni] viene generato dal compilatore - una nuova classe - <OuterClass>$<InnerClass>.classviene creato e i parametri associati vengono passati nel costruttore [Variabile locale nello stack] . È simile alla chiusura

la variabile finale è una variabile che non può essere riassegnata. la variabile di riferimento finale può ancora essere modificata modificando uno stato

Se fosse possibile, sarebbe strano perché come programmatore potresti fare così

//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;
}

-2

Forse questo trucco ti dà un'idea

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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.