web-dev-qa-db-fra.com

Fuite dans l'avertissement du constructeur

J'aimerais éviter (la plupart des) les avertissements de Netbeans 6.9.1, et j'ai un problème avec l'avertissement 'Leaking this in constructor'.

Je comprends le problème, appeler une méthode dans le constructeur et passer "this" est dangereux, car "this" n’a peut-être pas été entièrement initialisé.

Il était facile de corriger l'avertissement dans mes classes singleton, car le constructeur est privé et appelé uniquement à partir de la même classe.

Ancien code (simplifié):

private Singleton() {
  ...
  addWindowFocusListener(this);
}

public static Singleton getInstance() {

  ...
  instance = new Singleton();
  ...
}

Nouveau code (simplifié):

private Singleton() {
  ...
}

public static Singleton getInstance() {

  ...
  instance = new Singleton();
  addWindowFocusListener( instance );
  ...
}

Ce correctif ne fonctionne pas si le constructeur est public et peut être appelé à partir d'autres classes. Comment est-il possible de corriger le code suivant:

public class MyClass {

  ...
  List<MyClass> instances = new ArrayList<MyClass>();
  ...

  public MyClass() {
    ...
    instances.add(this);
  }

}

Bien sûr, je veux un correctif qui ne nécessite pas de modifier tous mes codes en utilisant cette classe (en appelant une méthode init par exemple).

75
asalamon74

Puisque vous vous assurez de mettre votre instances.add(this) à la fin du constructeur, vous iMHO devrait-il être prudent de dire au compilateur de supprimer simplement l’avertissement (*) . Un avertissement, de par sa nature, ne signifie pas nécessairement qu'il y a quelque chose qui ne va pas, il nécessite simplement votre attention.

Si vous savez ce que vous faites, vous pouvez utiliser une annotation @SuppressWarnings. Comme Terrel l'a mentionné dans ses commentaires, l'annotation suivante le fait à partir de NetBeans 6.9.1:

@SuppressWarnings("LeakingThisInConstructor")

(*) Mise à jour: Comme Isthar et Sergey l'ont souligné, il est parfois "fuyant" du code constructeur qui peut sembler parfaitement sûr (comme dans votre question), mais ce n'est pas le cas. Y a-t-il plus de lecteurs qui peuvent approuver cela? J'envisage de supprimer cette réponse pour les raisons mentionnées.

43
chiccodoro

[Remarque de chiccodoro: Une explication pourquoi/quand une fuite this peut causer des problèmes, même si l'instruction qui fuit est placée en dernier dans le constructeur:]

La sémantique de champ finale est différente de la sémantique de champ 'normale'. Un exemple, 

Nous jouons à un jeu en réseau. Permet de créer un objet de jeu récupérant des données du réseau et un objet de joueur écoutant les événements du jeu pour agir en conséquence. L'objet du jeu cache tous les détails du réseau, le joueur ne s'intéresse qu'aux événements:

import Java.util.*;
import Java.util.concurrent.Executors;

public class FinalSemantics {

    public interface Listener {
        public void someEvent();
    }

    public static class Player implements Listener {
        final String name;

        public Player(Game game) {
            name = "Player "+System.currentTimeMillis();
            game.addListener(this);//Warning leaking 'this'!
        }

        @Override
        public void someEvent() {
            System.out.println(name+" sees event!");
        }
    }

    public static class Game {
        private List<Listener> listeners;

        public Game() {
            listeners = new ArrayList<Listener>();
        }

        public void start() {
            Executors.newFixedThreadPool(1).execute(new Runnable(){

                @Override
                public void run() {
                    for(;;) {
                        try {
                            //Listen to game server over network
                            Thread.sleep(1000); //<- think blocking read

                            synchronized (Game.this) {
                                for (Listener l : listeners) {
                                    l.someEvent();
                                }
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }            
            });
        }

        public synchronized void addListener(Listener l) {
            listeners.add(l);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Game game = new Game();
        game.start();
        Thread.sleep(1000);
        //Someone joins the game
        new Player(game);
    }
}
//Code runs, won't terminate and will probably never show the flaw.

Semble tout bon: l'accès à la liste est correctement synchronisé. La faille est que cet exemple fuit Player.this to Game, qui exécute un fil. 

La finale est assez effrayante :

... les compilateurs ont beaucoup de liberté pour déplacer les lectures des champs finaux à travers les barrières de synchronisation ...

Cela élimine pratiquement toute synchronisation appropriée. Mais heureusement 

Un thread qui ne peut voir qu'une référence à un objet après que cet objet a été initialisé complètement est garanti pour voir les valeurs correctement initialisées pour les champs final de cet objet. 

Dans l'exemple, le constructeur écrit la référence aux objets dans la liste. (Et donc n'a pas encore été complètement initialisé, car le constructeur n'a pas fini.) Après l'écriture, le constructeur n'est toujours pas terminé. Il doit juste revenir du constructeur, mais supposons que ce ne soit pas encore le cas. Maintenant, l'exécuteur peut faire son travail et diffuser des événements à tous les auditeurs, y compris l'objet du lecteur non encore initialisé! Le dernier champ du joueur (nom) ne peut pas être écrit, et entraînera l'impression null sees event!

34
Ishtar

Les meilleures options que vous avez:

  • Extrayez votre pièce WindowFocusListener dans une autre classe (peut aussi être interne ou anonyme). La meilleure solution, de cette façon, chaque classe a un objectif spécifique.
  • Ignorer le message d'avertissement.

Utiliser un singleton comme solution de contournement pour un constructeur qui fuit n'est pas vraiment efficace.

13
Colin Hebert

C'est un bon cas où une fabrique qui a créé des instances de votre classe serait utile. Si une Factory était responsable de la création des instances de votre classe, vous auriez alors un emplacement centralisé où le constructeur est appelé et il serait trivial d'ajouter une méthode init() obligatoire à votre code.

En ce qui concerne votre solution immédiate, je vous suggère de déplacer tous les appels laissant this vers la dernière ligne de votre constructeur, puis de les supprimer avec une annotation une fois que vous avez "prouvé" que vous pouvez le faire en toute sécurité.

Dans IntelliJ IDEA, vous pouvez supprimer cet avertissement avec le commentaire suivant juste au-dessus de la ligne:
//noinspection ThisEscapedInObjectConstruction 

12
Nate W.

On peut écrire: 

addWindowFocusListener(Singleton.this);

Cela empêchera NB d'afficher l'avertissement.

4
Andrew

Utiliser une classe imbriquée (comme suggéré par Colin) est probablement votre meilleure option. Voici le pseudocode:

private Singleton() {
  ...
}

public static Singleton getInstance() {

  ...
  instance = new Singleton();
  addWindowFocusListener( new MyListener() );
  ...

  private class MyListener implements WindowFocusListener {
  ...
  }
}
2
Ron Stern

Il n'y a pas besoin de classe d'auditeur séparée.

public class Singleton implements WindowFocusListener {

    private Singleton() {
      ...
    }    

    private void init() {
      addWindowFocusListener(this);
    }

    public static Singleton getInstance() {    
      ...
      if(instance != null) {
        instance = new Singleton();
        instance.init();
      }
      ...
    }
}
2
jkuruvila

L'annotation @SuppressWarnings ("LeakingThisInConstructor") applicable uniquement à la classe et non au constructeur lui-même.

Solusion Je suggérerais: Create private method init () {/ * utilise this ici * /} et appelle-le depuis le constructeur. Les NetBeans ne vous préviendront pas.

1
CAB

Supposons que vous ayez à l'origine une classe comme celle-ci qui s'est utilisée comme ActionListener et que vous appelez donc addActionListener (this), ce qui génère l'avertissement.

private class CloseWindow extends JFrame implements ActionListener {
    public CloseWindow(String e) {
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        setLayout(new BorderLayout());

        JButton exitButton = new JButton("Close");
        exitButton.addActionListener(this);
        add(exitButton, BorderLayout.SOUTH);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        String actionCommand = e.getActionCommand();

        if(actionCommand.equals("Close")) {
            dispose();
        }
    }
}

Comme @Colin Hebert l'a mentionné, vous pouvez séparer ActionListener dans sa propre classe. Bien sûr, cela nécessiterait alors une référence au JFrame sur lequel vous voulez appeler .dispose (). Si vous préférez ne pas remplir votre espace de nom de variable et que vous souhaitez pouvoir utiliser ActionListener pour plusieurs JFrames, vous pouvez le faire avec getSource () pour récupérer le bouton suivi d'une chaîne d'appels getParent () à récupérez la classe qui étend JFrame, puis appelez getSuperclass pour vous assurer qu'il s'agit bien de JFrame.

private class CloseWindow extends JFrame {
    public CloseWindow(String e) {
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        setLayout(new BorderLayout());

        JButton exitButton = new JButton("Close");
        exitButton.addActionListener(new ExitListener());
        add(exitButton, BorderLayout.SOUTH);
    }
}

private class ExitListener implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        String actionCommand = e.getActionCommand();
        JButton sourceButton = (JButton)e.getSource();
        Component frameCheck = sourceButton;
        int i = 0;            
        String frameTest = "null";
        Class<?> c;
        while(!frameTest.equals("javax.swing.JFrame")) {
            frameCheck = frameCheck.getParent();
            c = frameCheck.getClass();
            frameTest = c.getSuperclass().getName().toString();
        }
        JFrame frame = (JFrame)frameCheck;

        if(actionCommand.equals("Close")) {
            frame.dispose();
        }
    }
}

Le code ci-dessus fonctionnera pour n'importe quel bouton qui est un enfant à n'importe quel niveau d'une classe qui étend JFrame. Évidemment, si votre objet est simplement un JFrame, il vous suffit de vérifier cette classe directement plutôt que de vérifier la super classe.

En fin de compte, en utilisant cette méthode, vous obtenez une référence à quelque chose comme ceci: MainClass $ CloseWindow qui contient la super classe JFrame, puis vous lancez cette référence à JFrame et vous en débarrassez.

0
sage88

Enveloppez votre this entre des crochets doubles. Netbeans ignore certaines erreurs par défaut si elles sont dans des sous-instructions. 

  public MyClass() {
     ...
     instances.add((this));
  }

https://stackoverflow.com/a/8357990

0
user7868