web-dev-qa-db-fra.com

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

  1. a ne peut être final ici. Pourquoi? Comment puis-je réaffecter a dans la méthode onClick() sans la conserver 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 * a quand 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 
            }
        });
    }
    
336
user467871

Comme indiqué dans les commentaires, une partie de cela devient sans objet dans Java 8, où final peut être implicite. Seule une variable finale effectivement peut être utilisée dans une classe interne anonyme ou une expression lambda.


C'est essentiellement dû à la façon dont Java gère 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 de générer automatiquement divers types supplémentaires pour conserver l’état logique des "variables locales", comme par exemple le compilateur C # ... (Lorsque C # capture une variable dans une fonction anonyme, il capture réellement la variable - la fermeture peut mettre à jour la variable d’une manière visible par le corps principal de la méthode, et inversement.)

Comme la valeur a été copiée dans l’instance de la classe interne anonyme, il serait étrange que la variable puisse être modifiée par le reste de la méthode - vous pourriez avoir du code qui semble fonctionner avec une variable périmée ( car c’est effectivement ce qui se produirait … vous travailleriez avec une copie prise à un moment différent). 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 de tels changements seront visibles. La seule façon de permettre à la méthode et à la classe interne anonyme de voir les modifications de l'autre consiste à utiliser un type modifiable d'une certaine description. Cela pourrait être la classe englobante elle-même, un tableau, un type de wrapper mutable ... quelque chose comme ça. En gros, cela ressemble un peu à la communication d’une méthode à l’autre: les modifications apportées aux paramètres d’une méthode ne sont pas vues par son appelant, mais les modifications apportées à les objets référencés par les paramètres sont vus.

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

469
Jon Skeet

Une astuce permet à une classe anonyme de mettre à jour des données dans l'étendue 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 à cause 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 la résolution a été mise à jour

Cette astuce fonctionne cependant si une classe anonyme est invoquée immédiatement 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]);

// ...
42
Ivan Dubrov

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

Toute variable locale, paramètre de méthode formelle ou paramètre de gestionnaire d'exceptions 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 ni d'explication sur les jls ou jvms, mais nous savons que le compilateur crée un fichier de classe séparé pour chaque classe interne et qu'il doit s'assurer que les méthodes déclarées dans ce fichier de classe ( au niveau du code d'octet) ont au moins accès aux valeurs des variables locales.

( Jon a la réponse complète - Je garde celui-ci non supprimé car on pourrait être intéressé par la règle JLS)

17
Andreas_D

Vous pouvez créer une variable de niveau de 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;
        }
    });
}

maintenant, vous pouvez 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 la méthode qui la contient. Lorsque l'instance utilise un local final de la méthode qui la contient, 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 réalité de la version brute et limitée de Java de fermetures).

Comme une classe interne locale n'est ni le membre d'une classe ni 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 similaires à ceux d'une classe normale.)

10
Ashfak Balooch

En Java, une variable peut être finale non seulement en tant que paramètre, mais également en tant que champ de niveau classe, comme

public class Test
{
 public final int a = 3;

ou comme une variable locale, comme

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

Si vous souhaitez accéder à une variable d'une classe anonyme et la modifier, vous pouvez définir la variable comme une variable niveau de classe dans la classe englobant.

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 finale et lui donner une nouvelle valeur. final signifie simplement que: la valeur est immuable et définitive.

Et comme c'est final, Java peut en toute sécurité copier le 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, mais simplement à Objets).

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

6
Zach L

La raison pour laquelle l'accès a été restreint aux seules variables finales locales est que si toutes les variables locales étaient 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, un nombre quelconque de copies n’aura aucun impact sur la cohérence des données.

6
Swapnil Gangrade

Pour comprendre la raison 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'interface reste en mémoire après le retour de la méthode initialize , mais le paramètre val ne le fait pas. La machine virtuelle Java ne peut pas accéder à une variable locale hors de sa portée. Java exécute ensuite l'appel à printInteger en copiant la valeur. de val à un champ implicite du même nom dans interfaceInstance . Le interfaceInstance est réputé avoir capturé la valeur du paramètre local. Si le paramètre n’est pas final (ou effectivement final), sa valeur peut changer et devenir désynchronisée par rapport à la valeur capturée, ce qui peut entraîner un comportement non intuitif.

3

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 depuis 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 sous forme de champ dans la classe interne. Une fois que la méthode externe est terminée et que son cadre de pile a été supprimé, la variable d'origine a disparu, mais la copie privée de la classe interne persiste dans sa propre mémoire.

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

2
ola_star

Les méthodes au sein d'une classe interne anonyme peuvent être invoquées bien après la fin du thread qui l'a générée. Dans votre exemple, la classe interne sera appelée sur le thread de répartition des é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’attribution de variable, vous devez les déclarer final.

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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}
1
Bruce Zu

Essayez ce code,

Créez une liste de tableaux, mettez-y une valeur et renvoyez-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;
}
0
Wasim

Comme Jon les détails de l'implémentation répondent-ils? Une autre réponse possible serait que la JVM ne veuille pas gérer l'écriture dans l'enregistrement qui a mis fin à son activation.

Considérez le cas d'utilisation où vos lambdas au lieu d'être appliqués sont stockés à 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.

0
mathk