web-dev-qa-db-fra.com

Toutes les variables finales sont-elles capturées par des classes anonymes?

Je pensais connaître la réponse à cette question, mais je ne trouve aucune confirmation après environ une heure de recherche.

Dans ce code:

public class Outer {

    // other code

    private void method1() {
        final SomeObject obj1 = new SomeObject(...);
        final SomeObject obj2 = new SomeObject(...);
        someManager.registerCallback(new SomeCallbackClass() {
            @Override
            public void onEvent() {
                 System.out.println(obj1.getName());
            }
        });
    }
}

Supposons que registerCallback enregistre son paramètre quelque part, de sorte que l'objet de la sous-classe anonyme vive pendant un certain temps. Il est évident que cet objet doit conserver une référence à obj1 pour que onEvent fonctionne s'il est appelé.

Mais étant donné que l'objet n'utilise pas obj2, conserve-t-il toujours une référence à obj2 afin que obj2 ne puisse pas être nettoyé pendant que l'objet est en vie? J'avais l'impression que tous visible final (ou effectivement final) les variables et paramètres locaux ont été capturés et ne peuvent donc pas être GC'ed tant que l'objet est en vie, mais je ne trouve rien d'une façon ou d'une autre.

Est-ce que cela dépend de la mise en œuvre?

Y at-il une section dans le JLS qui répond à cela? Je n'ai pas pu trouver la réponse ici.

17
ajb

La spécification de langue a très peu à dire sur la façon dont les classes anonymes devraient capturer des variables à partir de leur portée englobante.

La seule section de la spécification de langue que je puisse trouver qui soit particulièrement pertinente est JLS Sec 8.1.3 :

Toute variable locale, paramètre formel ou paramètre d'exception utilisé mais non déclaré dans une classe interne doit soit être déclaré final, soit être réellement final (§4.12.4), sinon une erreur de compilation survient lors de la tentative d'utilisation.

( Les classes anonymes sont des classes intérieures )

Il ne spécifie rien sur les variables que la classe anonyme doit capturer, ni sur la manière dont cette capture doit être implémentée.

Je pense qu'il est raisonnable d'en déduire que les implémentations ne doivent pas capturer des variables qui ne sont pas référencées dans la classe interne; mais cela ne dit pas qu'ils ne peuvent pas.

12
Andy Turner

Seul obj1 est capturé.

Logiquement , la classe anonyme est implémentée comme une classe normale, quelque chose comme ceci:

class Anonymous1 extends SomeCallbackClass {
    private final Outer _outer;
    private final SomeObject obj1;
    Anonymous1(Outer _outer, SomeObject obj1) {
        this._outer = _outer;
        this.obj1 = obj1;
    }
    @Override
    public void onEvent() {
         System.out.println(this.obj1.getName());
    }
});

Notez qu'une classe anonyme est toujours une classe interne, elle conservera donc toujours une référence à la classe externe, même si elle n'en a pas besoin. Je ne sais pas si les versions ultérieures du compilateur ont optimisé cela, mais je ne le pense pas. C'est une cause potentielle de fuites de mémoire.

Son utilisation devient:

someManager.registerCallback(new Anonymous1(this, obj1));

Comme vous pouvez le constater, la valeur de référence de obj1 est copied (pass-by-value).

Il n'y a techniquement aucune raison pour que obj1 soit final, qu'il soit déclaré final ou effectivement final (Java 8+), sauf que si ce n'est pas le cas et que vous modifiez la valeur, la copie ne changera pas, ce qui entraînerait des bugs vous vous attendiez à ce que la valeur change, étant donné que la copie est une action cachée. Pour éviter toute confusion chez les programmeurs, ils ont décidé que obj1 devait être final, afin que vous ne puissiez jamais vous confondre à propos de ce comportement.

10
Andreas

J'étais curieux et surpris par votre affirmation que beaucoup (pourquoi le compilateur ferait une telle chose ???), que je devais le vérifier moi-même. J'ai donc fait un exemple simple comme celui-ci

public class test {
    private static Object holder;

    private void method1() {
        final Object obj1 = new Object();
        final Object obj2 = new Object();
        holder = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println(obj1);
            }
        };
    }
}

Et résulté avec le bytecode suivant pour de method1

 private method1()V
   L0
    LINENUMBER 8 L0
    NEW Java/lang/Object
    DUP
    INVOKESPECIAL Java/lang/Object.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 9 L1
    NEW Java/lang/Object
    DUP
    INVOKESPECIAL Java/lang/Object.<init> ()V
    ASTORE 2
   L2
    LINENUMBER 10 L2
    NEW test$1
    DUP
    ALOAD 0
    ALOAD 1
    INVOKESPECIAL test$1.<init> (Ltest;Ljava/lang/Object;)V
    PUTSTATIC test.holder : Ljava/lang/Object;

Ce qui signifie:

  • L0 - stocker la première finale avec idx 1 (ASTORE 1)
  • L1 - stocker la seconde finale avec idx 2 (celle-ci n’est pas utilisée dans une classe) (ASTORE 2)
  • L2 - créer un nouveau test $ 1 avec argumets (ALOAD 0) this et obj1 (ALOAD 1)

Donc, je ne sais pas du tout, comment en êtes-vous arrivé à la conclusion que obj2 est passé à l'instance de classe anonyme, mais que c'était tout simplement faux. IDK s’il est dépendant du compilateur, mais pour ce que les autres ont dit, ce n’est pas impossible. 

2
Antoniossss

obj2 sera récupéré car il n'y est pas référencé. obj1 ne sera pas nettoyé tant que l'événement est actif car, même si vous avez créé une classe anonyme, vous avez créé une référence directe à obj1. 

La seule chose que vous avez à faire est que vous ne pouvez pas redéfinir la valeur, cela ne protège pas l'objet du ramasse-miettes.

0
Nertan Lucian