Pourquoi seules les variables finales sont-elles accessibles dans une classe anonyme?

355
  1. ane peut être définitif ici. Pourquoi? Comment puis - je réattribuer aen onClick()méthode sans le garder en tant que membre privé?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
  2. Comment puis-je retourner le 5 * aquand il a cliqué? Je veux dire,

    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
la source
1
Je ne pense pas que les classes anonymes Java fournissent le type de fermeture lambda que vous attendez, mais quelqu'un s'il vous plaît corrigez-moi si je me trompe ...
user541686
4
Qu'essayez-vous de réaliser? Le gestionnaire de clic peut être exécuté lorsque "f" est terminé.
Ivan Dubrov
@Lambert si vous souhaitez utiliser un dans la méthode onClick, il doit être définitif @Ivan comment la méthode f () peut-elle se comporter comme la méthode onClick () retourner int lorsqu'elle est cliquée
2
C'est ce que je veux dire - il ne prend pas en charge la fermeture complète car il ne permet pas d'accéder aux variables non finales.
user541686
4
Remarque: à partir de Java 8, votre variable doit seulement être effectivement définitive
Peter Lawrey

Réponses:

489

Comme indiqué dans les commentaires, certains de ces éléments ne sont plus pertinents dans Java 8, où ils finalpeuvent être implicites. Cependant, seule une variable effectivement finale peut être utilisée dans une classe interne anonyme ou une expression lambda.


C'est essentiellement dû à la façon dont Java gère les fermetures .

Lorsque vous créez une instance d'une classe interne anonyme, toutes les variables utilisées dans cette classe ont leurs valeurs copiées via le constructeur généré automatiquement. Cela évite au compilateur d'avoir à générer automatiquement divers types supplémentaires pour conserver l'état logique des "variables locales", comme par exemple le compilateur C # le fait ... (Lorsque C # capture une variable dans une fonction anonyme, il capture vraiment la variable - le la fermeture peut mettre à jour la variable d'une manière qui est vue par le corps principal de la méthode, et vice versa.)

Comme la valeur a été copiée dans l'instance de la classe interne anonyme, il semblerait étrange que la variable puisse être modifiée par le reste de la méthode - vous pourriez avoir du code qui semblait fonctionner avec une variable obsolète ( car c'est effectivement ce qui se passerait ... vous travaillez avec une copie prise à un autre moment). De même, si vous pouviez apporter des modifications dans la classe interne anonyme, les développeurs pourraient s'attendre à ce que ces modifications soient visibles dans le corps de la méthode englobante.

Rendre la variable finale supprime toutes ces possibilités - comme la valeur ne peut pas être modifiée du tout, vous n'avez pas à vous soucier de savoir si ces changements seront visibles. La seule façon d'autoriser la méthode et la classe interne anonyme à voir les modifications de l'autre est d'utiliser un type mutable d'une description. Cela pourrait être la classe englobante elle-même, un tableau, un type d'emballage mutable ... quelque chose comme ça. Fondamentalement, c'est un peu comme communiquer entre une méthode et une autre: les modifications apportées aux paramètres d'une méthode ne sont pas visibles par son appelant, mais les modifications apportées aux objets référencés par les paramètres sont visibles.

Si vous êtes intéressé par une comparaison plus détaillée entre les fermetures Java et C #, j'ai un article qui va plus loin. Je voulais me concentrer sur le côté Java dans cette réponse :)

Jon Skeet
la source
4
@Ivan: Comme C #, en gros. Cependant, il est assez complexe, si vous voulez le même type de fonctionnalité que C # où les variables de différentes étendues peuvent être "instanciées" un nombre de fois différent.
Jon Skeet
11
Tout cela était vrai pour Java 7, gardez à l'esprit qu'avec Java 8, des fermetures ont été introduites et il est désormais possible d'accéder à un champ non final d'une classe à partir de sa classe interne.
Mathias Bader
22
@MathiasBader: Vraiment? Je pensais que c'était toujours essentiellement le même mécanisme, le compilateur est maintenant juste assez intelligent pour en déduire final(mais il doit toujours être effectivement final).
Thilo
3
@Mathias Bader: vous pouvez toujours accéder aux champs non finaux , qui ne doivent pas être confondus avec les variables locales , qui devaient être finales et doivent encore être effectivement finales, donc Java 8 ne change pas la sémantique.
Holger
41

Il existe une astuce qui permet à la classe anonyme de mettre à jour les données dans la portée externe.

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

Cependant, cette astuce n'est pas très bonne en raison des problèmes de synchronisation. Si le gestionnaire est appelé plus tard, vous devez 1) synchroniser l'accès à res si le gestionnaire a été appelé à partir du thread différent 2) avoir besoin d'une sorte d'indicateur ou d'indication que res a été mis à jour

Cette astuce fonctionne bien, cependant, si une classe anonyme est immédiatement invoquée dans le même thread. Comme:

// ...

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
la source
2
Merci pour votre réponse. Je sais tout cela et ma solution est meilleure que ça. ma question est "pourquoi seulement finale"?
6
La réponse est alors de savoir comment ils sont mis en œuvre :)
Ivan Dubrov
1
Merci. J'avais utilisé l'astuce ci-dessus par moi-même. Je ne savais pas si c'était une bonne idée. Si Java ne le permet pas, il peut y avoir une bonne raison. Votre réponse précise que mon List.forEachcode est sûr.
RuntimeException
Lisez stackoverflow.com/q/12830611/2073130 pour une bonne discussion de la raison d'être du "pourquoi seulement final".
lcn
il existe plusieurs solutions de contournement. Le mien est: final int resf = res; Au départ, j'ai utilisé l'approche tableau, mais je trouve qu'elle a trop de syntaxe encombrante. AtomicReference est peut-être un peu plus lent (alloue un objet).
zakmck
17

Une classe anonyme est une classe interne et la règle stricte s'applique à classes internes (JLS 8.1.3) :

Toute variable locale, paramètre de méthode formelle ou paramètre de gestionnaire d'exception utilisé mais non déclaré dans une classe interne doit être déclaré final . Toute variable locale, utilisée mais non déclarée dans une classe interne doit être définitivement affectée avant le corps de la classe interne .

Je n'ai pas encore trouvé de raison ou d'explication sur les jls ou les jvms, mais nous savons que le compilateur crée un fichier de classe distinct pour chaque classe interne et qu'il doit s'assurer que les méthodes déclarées sur ce fichier de classe ( au niveau du code octet) ont au moins accès aux valeurs des variables locales.

( Jon a la réponse complète - je garde celle-ci non supprimée car on pourrait s'intéresser à la règle JLS)

Andreas Dolk
la source
11

Vous pouvez créer une variable de niveau classe pour obtenir la valeur renvoyée. je veux dire

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

vous pouvez maintenant obtenir la valeur de K et l'utiliser où vous le souhaitez.

La réponse de votre pourquoi est:

Une instance de classe interne locale est liée à la classe Main et peut accéder aux variables locales finales de sa méthode conteneur. Lorsque l'instance utilise un local final de sa méthode conteneur, la variable conserve la valeur qu'elle détenait au moment de la création de l'instance, même si la variable est hors de portée (il s'agit en fait de la version brute et limitée de fermetures de Java).

Parce qu'une classe interne locale n'est ni le membre d'une classe ou d'un package, elle n'est pas déclarée avec un niveau d'accès. (Soyez clair, cependant, que ses propres membres ont des niveaux d'accès comme dans une classe normale.)

Ashfak Balooch
la source
J'ai mentionné que "sans le garder en tant que membre privé"
6

Eh bien, en Java, une variable peut être finale non seulement en tant que paramètre, mais en tant que champ au niveau de la classe, comme

public class Test
{
 public final int a = 3;

ou en tant que variable locale, comme

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

Si vous souhaitez accéder à une variable à partir d'une classe anonyme et la modifier, vous pouvez faire de la variable une variable de niveau classe dans la classe englobante .

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

Vous ne pouvez pas avoir une variable comme finale et lui donner une nouvelle valeur. finalsignifie juste cela: la valeur est immuable et définitive.

Et comme il est définitif, Java peut le copier en toute sécurité dans des classes anonymes locales. Vous n'obtenez pas de référence à l'int (surtout que vous ne pouvez pas avoir de références à des primitives comme int en Java, juste des références à des objets ).

Il copie simplement la valeur de a dans un int implicite appelé a dans votre classe anonyme.

Zach L
la source
3
J'associe "variable de niveau classe" à static. Il est peut-être plus clair si vous utilisez plutôt "variable d'instance".
eljenso
1
eh bien, j'ai utilisé au niveau de la classe parce que la technique fonctionnerait avec les variables d'instance et statiques.
Zach L
on sait déjà que la finale est accessible mais on veut savoir pourquoi? pouvez-vous s'il vous plaît ajouter quelques explications sur le côté pourquoi?
Saurabh Oza
6

La raison pour laquelle l'accès a été limité uniquement aux variables finales locales est que si toutes les variables locales devaient être rendues accessibles, elles devraient d'abord être copiées dans une section distincte où les classes internes peuvent y avoir accès et conserver plusieurs copies de les variables locales mutables peuvent conduire à des données incohérentes. Alors que les variables finales sont immuables et que le nombre de copies qu'elles contiennent n'aura donc aucun impact sur la cohérence des données.

Swapnil Gangrade
la source
Ce n'est pas ainsi qu'il est implémenté dans des langages comme C # qui prennent en charge cette fonctionnalité. En fait, le compilateur change la variable d'une variable locale en une variable d'instance, ou il crée une structure de données supplémentaire pour ces variables qui peut dépasser la portée de la classe externe. Cependant, il n'y a pas de "copies multiples de variables locales"
Mike76
Mike76 Je n'ai pas regardé l'implémentation de C #, mais Scala fait la deuxième chose que vous avez mentionnée, je pense: si un Intest en cours de réaffectation à l'intérieur d'une fermeture, changez cette variable en une instance de IntRef(essentiellement un Integerwrapper mutable ). Chaque accès variable est ensuite réécrit en conséquence.
Adowrath du
3

Pour comprendre la justification de cette restriction, envisagez le programme suivant:

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 reste en mémoire après le retour de la méthode d' initialisation , mais pas le paramètre val . La machine virtuelle Java ne peut pas accéder à une variable locale en dehors de sa portée, donc Java effectue l'appel suivant à printInteger en copiant la valeur de val dans un champ implicite du même nom dans interfaceInstance . L' interfaceInstance aurait capturé la valeur du paramètre local. Si le paramètre n'était pas définitif (ou effectivement définitif), sa valeur pourrait changer, se désynchroniser avec la valeur capturée, provoquant potentiellement un comportement peu intuitif.

Benjamin Curtis Drake
la source
2

Les méthodes au sein d'une classe interne anonome peuvent être invoquées bien après la fin du thread qui l'a engendrée. Dans votre exemple, la classe interne sera invoquée sur le thread de répartition d'événements et non dans le même thread que celui qui l'a créée. Par conséquent, la portée des variables sera différente. Donc, pour protéger ces problèmes de portée d'affectation de variables, vous devez les déclarer définitifs.

S73417H
la source
2

Lorsqu'une classe interne anonyme est définie dans le corps d'une méthode, toutes les variables déclarées finales dans la portée de cette méthode sont accessibles à partir de la classe interne. Pour les valeurs scalaires, une fois affectée, la valeur de la variable finale ne peut pas changer. Pour les valeurs d'objet, la référence ne peut pas changer. Cela permet au compilateur Java de "capturer" la valeur de la variable au moment de l'exécution et de stocker une copie en tant que champ dans la classe interne. Une fois la méthode externe terminée et son cadre de pile supprimé, la variable d'origine a disparu mais la copie privée de la classe interne persiste dans la mémoire de la classe.

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

ola_star
la source
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
la source
0

Comme Jon a les détails de l'implémentation, une autre réponse possible serait que la JVM ne veut pas gérer les enregistrements en écriture qui ont mis fin à son activation.

Considérez le cas d'utilisation où vos lambdas au lieu d'être appliqués, sont stockés dans un endroit et exécutés plus tard.

Je me souviens que dans Smalltalk, vous obtiendrez un magasin illégal lorsque vous effectuez une telle modification.

mathk
la source
0

Essayez ce code,

Créez une liste de tableaux et mettez-y de la valeur et retournez-la:

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
la source
OP demande des raisons pour lesquelles quelque chose est nécessaire. Par conséquent, vous devez indiquer comment votre code y fait face
NitinSingh
0

La classe anonyme Java est très similaire à la fermeture Javascript, mais Java l'implémente de manière différente. (vérifier la réponse d'Andersen)

Donc, afin de ne pas confondre le développeur Java avec le comportement étrange qui peut se produire pour ceux qui viennent de l'arrière-plan Javascript. Je suppose que c'est pourquoi ils nous obligent à utiliser final, ce n'est pas la limitation JVM.

Regardons l'exemple Javascript ci-dessous:

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

En Javascript, la countervaleur sera 100, car il n'y en a qu'uncounter variable du début à la fin.

Mais en Java, s'il n'y en a pas final, il s'imprime 0, car pendant la création de l'objet interne, la 0valeur est copiée dans les propriétés cachées de l'objet de classe interne. (il y a deux variables entières ici, une dans la méthode locale, une autre dans les propriétés cachées de la classe interne)

Donc, tout changement après la création de l'objet intérieur (comme la ligne 1), cela n'affectera pas l'objet intérieur. Cela créera donc une confusion entre deux résultats et comportements différents (entre Java et Javascript).

Je crois que c'est pourquoi, Java décide de le forcer à être définitif, de sorte que les données sont «cohérentes» du début à la fin.

GMsoF
la source
0

Variable finale Java à l'intérieur d'une classe interne

la classe interne ne peut utiliser que

  1. référence de la classe externe
  2. variables locales finales hors champ qui sont un type de référence (par exemple Object...)
  3. le type valeur (primitif) (par exemple int...) peut être encapsulé par un type de référence final. IntelliJ IDEApeut vous aider à le convertir en un tableau d' éléments

Lorsqu'un non static nested( inner class) [À propos] est généré par le compilateur - une nouvelle classe - <OuterClass>$<InnerClass>.classest créé et les paramètres liés sont passés au constructeur [Variable locale sur la pile] . C'est similaire à la fermeture

la variable finale est une variable qui ne peut pas être réaffectée. la variable de référence finale peut toujours être modifiée en modifiant un état

Il était possible que ce soit bizarre car en tant que programmeur, vous pourriez faire comme ça

//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
la source
-2

Peut-être que cette astuce vous donne une idée

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);
Whimusical
la source