661 votes

Pourquoi this() et super() doivent-ils être la première instruction d'un constructeur ?

Java exige que si vous appelez this() ou super() dans un constructeur, ce doit être la première déclaration. Pourquoi ?

Par exemple :

public class MyClass {
    public MyClass(int x) {}
}

public class MySubClass extends MyClass {
    public MySubClass(int a, int b) {
        int c = a + b;
        super(c);  // COMPILE ERROR
    }
}

Le compilateur Sun dit "l'appel à super doit être la première déclaration du constructeur". Le compilateur Eclipse dit "L'appel au constructeur doit être la première déclaration dans un constructeur".

Cependant, vous pouvez contourner ce problème en réorganisant un peu le code :

public class MySubClass extends MyClass {
    public MySubClass(int a, int b) {
        super(a + b);  // OK
    }
}

Voici un autre exemple :

public class MyClass {
    public MyClass(List list) {}
}

public class MySubClassA extends MyClass {
    public MySubClassA(Object item) {
        // Create a list that contains the item, and pass the list to super
        List list = new ArrayList();
        list.add(item);
        super(list);  // COMPILE ERROR
    }
}

public class MySubClassB extends MyClass {
    public MySubClassB(Object item) {
        // Create a list that contains the item, and pass the list to super
        super(Arrays.asList(new Object[] { item }));  // OK
    }
}

Donc, c'est ne vous empêche pas d'exécuter la logique avant l'appel au super. Il vous empêche simplement d'exécuter une logique que vous ne pouvez pas faire tenir dans une seule expression.

Il existe des règles similaires pour appeler this() . Le compilateur dit "l'appel à this doit être la première déclaration du constructeur".

Pourquoi le compilateur a-t-il ces restrictions ? Pouvez-vous donner un exemple de code où, si le compilateur n'avait pas cette restriction, quelque chose de mauvais se produirait ?

201 votes

Tellement de haine pour ça. Pourquoi ne peut-il pas y avoir une exception "objet pas encore construit", pour que je n'aie pas à tout mettre dans une seule ligne ?

9 votes

Une bonne question. J'ai commencé un projet similaire en valjok.blogspot.com/2012/09/ et programmers.exchange où je montre qu'il y a des cas où les sous-champs doivent être initialisés avant le super(). Ainsi, la fonctionnalité ajoute à la complexité de faire les choses alors qu'il n'est pas clair si les impacts positifs concernant la "sécurité du code" surpondèrent les négatifs. Oui, il y a des conséquences négatives à ce que super soit toujours le premier. Il est surprenant que personne ne le mentionne. Je pense que c'est une question conceptuelle qui doit être posée dans le cadre de l'échange de programmeurs.

3 votes

Merci de décrire clairement comment contourner cette restriction !

198voto

anio Points 3254

Le constructeur de la classe mère doit être appelé avant le constructeur de la sous-classe. Cela garantit que si vous appelez des méthodes de la classe mère dans votre constructeur, la classe mère a déjà été configurée correctement.

Ce que vous essayez de faire, passer les args au super constructeur est parfaitement légal, vous devez juste construire ces args en ligne comme vous le faites, ou les passer dans votre constructeur et ensuite les passer au super :

public MySubClassB extends MyClass {
        public MySubClassB(Object[] myArray) {
                super(myArray);
        }
}

Si le compilateur ne l'imposait pas, vous pourriez le faire :

public MySubClassB extends MyClass {
        public MySubClassB(Object[] myArray) {
                someMethodOnSuper(); //ERROR super not yet constructed
                super(myArray);
        }
}

Dans les cas où une classe parente possède un constructeur par défaut, l'appel à super est inséré automatiquement par le compilateur. Puisque chaque classe en Java hérite de Object Le constructeur de l'objet doit être appelé d'une manière ou d'une autre et doit être exécuté en premier. L'insertion automatique de super() par le compilateur permet cela. En forçant super à apparaître en premier, on s'assure que les corps des constructeurs sont exécutés dans l'ordre correct qui serait : Objet -> Parent -> Enfant -> EnfantOfChild -> SoOnSoForth

1 votes

Convenu. On ne peut rien faire avec l'objet tant que son parent n'est pas construit. Tu peux travailler avec d'autres choses, mais pas avec cet objet.

0 votes

Qu'en est-il des blocs d'initialisation anonymes ? consultez ma réponse.

0 votes

@Savvas L'initialisateur anonyme ne viole pas l'ordre des appels de constructeurs, il est donc correct. Le parent est construit en premier, puis le code de l'initialisateur anonyme est exécuté, donc c'est sûr. Les corps des constructeurs doivent être exécutés dans l'ordre suivant : Corps du constructeur de l'objet -> Corps du constructeur du parent -> Corps du constructeur de l'enfant Je pense que c'est pour faire respecter cet ordre que l'appel à super doit être le premier, de cette façon le corps de l'objet s'exécutera en premier puisqu'il n'a pas de super et ensuite il se déroulera et exécutera les autres corps du constructeur.

106voto

pendor Points 602

J'ai trouvé un moyen de contourner ce problème en enchaînant les constructeurs et les méthodes statiques. Ce que je voulais faire ressemblait à quelque chose comme ceci :

public class Foo extends Baz {
  private final Bar myBar;

  public Foo(String arg1, String arg2) {
    // ...
    // ... Some other stuff needed to construct a 'Bar'...
    // ...
    final Bar b = new Bar(arg1, arg2);
    super(b.baz()):
    myBar = b;
  }
}

En gros, il s'agit de construire un objet en fonction des paramètres du constructeur, de stocker l'objet dans un membre et de passer le résultat d'une méthode sur cet objet dans le constructeur de super. Rendre le membre final était aussi raisonnablement important car la nature de la classe est qu'elle est immuable. Notez qu'en fait, la construction de Bar prend quelques objets intermédiaires, donc ce n'est pas réductible à une ligne unique dans mon cas d'utilisation réel.

J'ai fini par faire en sorte que ça marche comme ça :

public class Foo extends Baz {
  private final Bar myBar;

  private static Bar makeBar(String arg1,  String arg2) {
    // My more complicated setup routine to actually make 'Bar' goes here...
    return new Bar(arg1, arg2);
  }

  public Foo(String arg1, String arg2) {
    this(makeBar(arg1, arg2));
  }

  private Foo(Bar bar) {
    super(bar.baz());
    myBar = bar;
  }
}

Code légal, et il accomplit la tâche d'exécuter plusieurs déclarations avant d'appeler le super constructeur.

0 votes

Cette technique peut être étendue. Si super prend de nombreux paramètres ou si vous devez définir d'autres champs en même temps, créez une classe interne statique pour contenir toutes les variables, et utilisez-la pour transmettre les données de la méthode statique au constructeur à argument unique.

21 votes

Pour info, très souvent, quand il semble que vous devez faire de la logique avant d'appeler super il est préférable d'utiliser la composition plutôt que l'héritage.

0 votes

Il m'a fallu un peu de temps avant de comprendre votre concept. Donc, fondamentalement, vous créez une méthode statique et la mettez dans le constructeur.

54voto

Parce que le JLS le dit. Le JLS pourrait-il être modifié de manière compatible pour l'autoriser ? Yup.

Cependant, cela compliquerait les spécifications du langage, qui sont déjà plus que compliquées. Ce ne serait pas une chose très utile à faire et il existe des moyens de contourner le problème (appeler un autre constructeur avec le résultat d'une méthode statique ou d'une expression lambda). this(fn()) - la méthode est appelée avant l'autre constructeur, et donc aussi le super constructeur). Le rapport poids/puissance de cette modification est donc défavorable.

Notez que cette règle seule n'empêche pas l'utilisation des champs avant que la super classe ait terminé sa construction.

Considérez ces exemples illégaux.

super(this.x = 5);

super(this.fn());

super(fn());

super(x);

super(this instanceof SubClass);
// this.getClass() would be /really/ useful sometimes.

Cet exemple est légal, mais "incorrect".

class MyBase {
    MyBase() {
        fn();
    }
    abstract void fn();
}
class MyDerived extends MyBase {
    void fn() {
       // ???
    }
}

Dans l'exemple ci-dessus, si MyDerived.fn les arguments requis de la MyDerived il faudrait les passer au crible d'un constructeur de ThreadLocal . ;(

D'ailleurs, depuis Java 1.4, le champ synthétique qui contient l'adresse externe de l'utilisateur est remplacé par une adresse externe. this est assigné avant que le super constructeur des classes internes soit appelé. Cela a causé des problèmes particuliers NullPointerException dans le code compilé pour cibler les versions antérieures.

Notez également qu'en présence d'une publication non sécurisée, la construction peut être vue réordonnée par d'autres fils, à moins que des précautions ne soient prises.

Édition mars 2018 : Dans le message Enregistrements : construction et validation Oracle propose de supprimer cette restriction (mais contrairement à C#, this sera définitivement non attribué (DU) avant le chaînage des constructeurs).

Historiquement, this() ou super() doit être le premier dans un constructeur. Cette restriction restriction n'a jamais été populaire, et perçue comme arbitraire. Il y avait un certain nombre de raisons subtiles, y compris la vérification de invokespecial, qui ont contribué à cette restriction. Au fil des années, nous avons abordé ces raisons au niveau de la VM, au point qu'il devient pratique d'envisager de lever cette restriction, non seulement pour les enregistrements mais pour tous les constructeurs.

1 votes

Juste pour clarifier : le fn() que vous avez utilisé dans votre exemple devrait être une méthode statique, n'est-ce pas ?

9 votes

+1 pour avoir mentionné qu'il s'agit d'une restriction purement JLS. Au niveau du bytecode, vous pouvez faire d'autres choses avant d'appeler un constructeur.

2 votes

Attendez, comment cela pourrait-il compliquer la spécification du langage ? Et à partir du moment où la spécification dit que la première déclaration peut être un constructeur, toutes les autres déclarations ne peuvent pas être des constructeurs. Quand vous enlevez la restriction, la spécification sera quelque chose comme "vous avez juste des déclarations à l'intérieur". En quoi est-ce plus compliqué ?

13voto

Jason S Points 58434

Je suis à peu près sûr (ceux qui connaissent bien la spécification Java peuvent nous en parler) que c'est pour vous empêcher (a) d'être autorisé à utiliser un objet partiellement construit, et (b) de forcer le constructeur de la classe parente à construire sur un objet "frais".

Voici quelques exemples d'une "mauvaise" chose :

class Thing
{
    final int x;
    Thing(int x) { this.x = x; }
}

class Bad1 extends Thing
{
    final int z;
    Bad1(int x, int y)
    {
        this.z = this.x + this.y; // WHOOPS! x hasn't been set yet
        super(x);
    }        
}

class Bad2 extends Thing
{
    final int y;
    Bad2(int x, int y)
    {
        this.x = 33;
        this.y = y; 
        super(x); // WHOOPS! x is supposed to be final
    }        
}

0 votes

Devrait Bad1 y Bad2 étendre Thing là ?

9 votes

Je ne suis pas d'accord avec Bad2 como x est déclaré dans Thing et ne doit tout simplement pas être défini ailleurs. Comme pour Bad1 Vous avez sûrement raison, mais une chose similaire peut se produire lorsque le super constructeur invoque une méthode surchargée dans la sous-classe qui accède à une variable (non encore initialisée) de la sous-classe. La restriction permet donc d'éviter une partie du problème... ce qui, à mon avis, n'en vaut pas la peine.

0 votes

@maaartinus la différence est que l'auteur du constructeur de la superclasse a la responsabilité concernant l'invocation des méthodes surchargées. Il est donc possible de concevoir la superclasse de manière à ce qu'elle ait toujours un état cohérent, ce qui ne serait pas possible si les sous-classes étaient autorisées à utiliser l'objet avant que le constructeur de la superclasse ait été appelé.

9voto

Kate Gregory Points 13451

Vous avez demandé pourquoi, et les autres réponses, à mon avis, ne disent pas vraiment pourquoi il est acceptable d'appeler le constructeur de votre super, mais seulement si c'est la toute première ligne. La raison est que vous n'êtes pas vraiment en appelant le constructeur. En C++, la syntaxe équivalente est

MySubClass: MyClass {

public:

 MySubClass(int a, int b): MyClass(a+b)
 {
 }

};

Lorsque vous voyez la clause d'initialisation seule comme ça, avant l'accolade ouverte, vous savez que c'est spécial. Elle s'exécute avant tout le reste du constructeur et, en fait, avant que les variables membres ne soient initialisées. Ce n'est pas si différent pour Java. Il existe un moyen de faire en sorte que du code (d'autres constructeurs) s'exécute avant que le constructeur ne démarre réellement, avant que les membres de la sous-classe ne soient initialisés. Ce moyen consiste à placer l'"appel" (ex. super ) sur la toute première ligne. (D'une certaine manière, ce super ou this est en quelque sorte avant la première accolade ouverte, même si vous le tapez après, car il sera exécuté avant que vous n'arriviez au point où tout est entièrement construit). Tout autre code après l'accolade ouverte (comme int c = a + b; ) fait dire au compilateur "oh, ok, pas d'autres constructeurs, nous pouvons tout initialiser alors". Il s'exécute donc et initialise votre super classe et vos membres et ainsi de suite, puis commence à exécuter le code après l'accolade ouverte.

Si, quelques lignes plus tard, il rencontre du code disant "oh oui, lorsque vous construisez cet objet, voici les paramètres que je veux que vous transmettiez au constructeur de la classe de base", il est trop tard et cela n'a aucun sens. Donc vous obtenez une erreur de compilation.

1 votes

1. Si les concepteurs de java voulaient que le superconstructeur soit implicite, ils pourraient simplement le faire et, plus important encore, cela n'explique pas pourquoi le superconstructeur implicite est très utile. 2. IMO, c'est votre commentaire que cela n'a pas de sens n'a pas de sens. Je me souviens que j'en avais besoin. Pouvez-vous prouver que j'ai fait quelque chose qui n'a pas de sens ?

2 votes

Imaginez que vous devez entrer dans une pièce. La porte est verrouillée, alors vous brisez une fenêtre, passez la main et entrez. À l'intérieur, à mi-chemin de la pièce, vous trouvez une note avec une clé que vous pouvez utiliser en entrant. Mais vous êtes déjà à l'intérieur. De la même manière, si le compilateur est à mi-chemin de l'exécution d'un constructeur et qu'il tombe sur "voici ce qu'il faut faire avec ces paramètres avant d'exécuter le constructeur", que doit-il faire ?

1 votes

Si c'est une chose stupide en réalité, alors c'est une mauvaise analogie. Si je suis en mesure de décider de la direction à prendre, je ne suis pas à mi-chemin. C'est la règle selon laquelle le supercall doit être le premier en construction qui nous pousse à briser la fenêtre (voir de nombreux exemples de détournement d'attention dans les questions et réponses) au lieu d'utiliser la porte. Ainsi, vous mettez tout à l'envers en essayant d'argumenter pour cette règle. La règle doit donc être fausse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X