web-dev-qa-db-fra.com

Quel est le problème avec les appels de méthode remplaçables dans les constructeurs?

J'ai une classe de page Wicket qui définit le titre de la page en fonction du résultat d'une méthode abstraite.

public abstract class BasicPage extends WebPage {

    public BasicPage() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();

}

NetBeans me met en garde avec le message "Appel de méthode overridable dans le constructeur", mais quel est le problème? La seule alternative que je puisse imaginer est de transmettre les résultats de méthodes par ailleurs abstraites au super constructeur des sous-classes. Mais cela pourrait être difficile à lire avec de nombreux paramètres.

363
deamon

Lors de l'appel d'une méthode pouvant être remplacée par des constructeurs

En termes simples, cela est faux car cela ouvre inutilement des possibilités de BEAUCOUP bugs. Lorsque le @Override est appelé, l'état de l'objet peut être incohérent et/ou incomplet.

Une citation de Effective Java 2nd Edition, élément 17: Conception et documentation pour héritage, ou bien l'interdire :

Il y a encore quelques restrictions auxquelles une classe doit obéir pour permettre l'héritage. Les constructeurs ne doivent pas invoquer de méthodes remplaçables , ni directement ni indirectement. Si vous ne respectez pas cette règle, le programme échouera. Le constructeur de la superclasse s'exécute avant le constructeur de la sous-classe. Par conséquent, la méthode de substitution de la sous-classe sera invoquée avant l'exécution du constructeur de la sous-classe. Si la méthode de substitution dépend d'une initialisation effectuée par le constructeur de la sous-classe, la méthode ne se comportera pas comme prévu.

Voici un exemple pour illustrer:

public class ConstructorCallsOverride {
    public static void main(String[] args) {

        abstract class Base {
            Base() {
                overrideMe();
            }
            abstract void overrideMe(); 
        }

        class Child extends Base {

            final int x;

            Child(int x) {
                this.x = x;
            }

            @Override
            void overrideMe() {
                System.out.println(x);
            }
        }
        new Child(42); // prints "0"
    }
}

Ici, lorsque Base constructeur appelle overrideMe, Child n'a pas fini d'initialiser le final int x, et la méthode obtient une valeur incorrecte. Cela mènera presque certainement à des bugs et des erreurs.

Questions connexes

Voir également


Sur la construction d'objet avec de nombreux paramètres

Les constructeurs avec de nombreux paramètres peuvent entraîner une mauvaise lisibilité et de meilleures alternatives existent.

Voici une citation de Effective Java 2nd Edition, élément 2: considérez un motif de générateur face à de nombreux paramètres de constructeur :

Traditionnellement, les programmeurs utilisaient le modèle de constructeur télescopique , dans lequel vous fournissez à un constructeur uniquement les paramètres requis, un autre avec un seul paramètre facultatif, un troisième avec deux paramètres facultatifs, etc. sur...

Le modèle de constructeur télescopique ressemble essentiellement à ceci:

public class Telescope {
    final String name;
    final int levels;
    final boolean isAdjustable;

    public Telescope(String name) {
        this(name, 5);
    }
    public Telescope(String name, int levels) {
        this(name, levels, false);
    }
    public Telescope(String name, int levels, boolean isAdjustable) {       
        this.name = name;
        this.levels = levels;
        this.isAdjustable = isAdjustable;
    }
}

Et maintenant, vous pouvez faire l’un des choix suivants:

new Telescope("X/1999");
new Telescope("X/1999", 13);
new Telescope("X/1999", 13, true);

Cependant, vous ne pouvez pas actuellement définir uniquement les variables name et isAdjustable, et laisser levels par défaut. Vous pouvez fournir davantage de surcharges de constructeur, mais le nombre explose à mesure que le nombre de paramètres augmente, et vous pouvez même disposer de plusieurs arguments boolean et int, ce qui gâcherait tout.

Comme vous pouvez le constater, ce modèle n’est pas agréable à écrire, et encore moins agréable à utiliser (que signifie "vrai" ici? Qu'est-ce qui est 13?).

Bloch recommande d'utiliser un modèle de générateur, ce qui vous permettrait d'écrire quelque chose comme ceci:

Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();

Notez que les paramètres sont maintenant nommés et que vous pouvez les définir dans l'ordre de votre choix et vous pouvez ignorer ceux que vous souhaitez conserver aux valeurs par défaut. C'est certainement beaucoup mieux que les constructeurs télescopiques, surtout quand il y a un grand nombre de paramètres qui appartiennent à beaucoup des mêmes types.

Voir également

Questions connexes

458
polygenelubricants

Voici un exemple qui aide à comprendre ceci:

public class Main {
    static abstract class A {
        abstract void foo();
        A() {
            System.out.println("Constructing A");
            foo();
        }
    }

    static class C extends A {
        C() { 
            System.out.println("Constructing C");
        }
        void foo() { 
            System.out.println("Using C"); 
        }
    }

    public static void main(String[] args) {
        C c = new C(); 
    }
}

Si vous exécutez ce code, vous obtenez le résultat suivant:

Constructing A
Using C
Constructing C

Vous voyez? foo() utilise C avant que le constructeur de C ait été exécuté. Si foo() requiert que C ait un état défini (c'est-à-dire le constructeur a terminé), il rencontrera un état non défini en C et des choses pourraient se briser. Et comme vous ne pouvez pas savoir en A ce que la foo() remplacée _ attend, vous recevez un avertissement.

56
Nordic Mainframe

L'appel d'une méthode remplaçable dans le constructeur permet aux sous-classes de subvertir le code, vous ne pouvez donc plus garantir qu'il fonctionne. C'est pourquoi vous recevez un avertissement.

Dans votre exemple, que se passe-t-il si une sous-classe remplace getTitle() et renvoie la valeur null?

Pour "réparer" ceci, vous pouvez utiliser une méthode d'usine à la place d'un constructeur, il s'agit d'un modèle courant d'instanciation d'objets.

11
KeatsPeeks

Si vous appelez des méthodes dans votre constructeur que les sous-classes remplacent, cela signifie que vous êtes moins susceptible de référencer des variables qui n'existent pas encore si vous divisez logiquement votre initialisation entre le constructeur et la méthode.

Jetez un coup d'œil à cet exemple de lien http://www.javapractices.com/topic/TopicAction.do?Id=215

4
Manuel Selva

Voici un exemple qui révèle les problèmes logiques pouvant survenir lors de l'appel d'une méthode pouvant être remplacée dans le super constructeur.

class A {

    protected int minWeeklySalary;
    protected int maxWeeklySalary;

    protected static final int MIN = 1000;
    protected static final int MAX = 2000;

    public A() {
        setSalaryRange();
    }

    protected void setSalaryRange() {
        throw new RuntimeException("not implemented");
    }

    public void pr() {
        System.out.println("minWeeklySalary: " + minWeeklySalary);
        System.out.println("maxWeeklySalary: " + maxWeeklySalary);
    }
}

class B extends A {

    private int factor = 1;

    public B(int _factor) {
        this.factor = _factor;
    }

    @Override
    protected void setSalaryRange() {
        this.minWeeklySalary = MIN * this.factor;
        this.maxWeeklySalary = MAX * this.factor;
    }
}

public static void main(String[] args) {
    B b = new B(2);
    b.pr();
}

Le résultat serait en réalité:

minWeeklySalary: 0

maxWeeklySalary: 0

Cela est dû au fait que le constructeur de la classe B appelle d'abord le constructeur de la classe A, où la méthode remplaçable à l'intérieur de B est exécutée. Mais à l'intérieur de la méthode, nous utilisons la variable d'instance facteur qui a pas encore initialisé (car le constructeur de A n'est pas encore terminé), donc facteur est 0 et non 1 et certainement pas 2 (ce que le programmeur pourrait penser que ce sera). Imaginez à quel point il serait difficile de suivre une erreur si la logique de calcul était dix fois plus déformée.

J'espère que cela aiderait quelqu'un.

4
kirilv

Dans le cas spécifique de Wicket: C’est la raison même pour laquelle j’ai demandé aux développeurs de Wicket d’ajouter un support pour un processus d’initialisation de composant explicite en deux phases dans le cycle de vie du cadre de construction d’un composant, c.-à-d.

  1. Construction - via constructeur
  2. Initialisation - via onInitilize (après la construction lorsque les méthodes virtuelles fonctionnent!)

Il y a eu un débat assez actif sur le fait de savoir si c'était nécessaire ou non (c'est tout à fait nécessaire à mon humble avis), comme le montre ce lien http://Apache-wicket.1842946.n4.nabble.com/VOTE-WICKET-3218- Component-onInitialize-is-broken-for-Pages-td3341090i20.html )

La bonne nouvelle est que les excellents développeurs de Wicket ont fini par introduire une initialisation en deux phases (pour rendre plus impressionnant le framework Java de l'interface utilisateur encore plus impressionnant!). Ainsi, avec Wicket, vous pouvez effectuer toutes vos initialisations post-construction dans La méthode onInitialize appelée automatiquement par le framework si vous le remplacez - à ce stade-ci du cycle de vie de votre composant, son constructeur a terminé son travail afin que les méthodes virtuelles fonctionnent comme prévu.

2
Volksman

Je suppose que pour Wicket, il est préférable d'appeler la méthode add dans la onInitialize() (voir cycle de vie des composants ):

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

    @Override
    public void onInitialize() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();
}
0
sanluck