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


Java requiert que si vous appelez this () ou super () dans un constructeur, il doit s'agir de la première instruction. 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 dans le constructeur". Le compilateur Eclipse dit "L'appel du constructeur doit être la première instruction dans un constructeur".

Cependant, vous pouvez contourner cela en réorganisant le code un peu:

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 pas vous empêcher d'exécuter la logique avant l'appel à super. Cela vous empêche simplement d'exécuter une logique que vous ne pouvez pas intégrer dans une seule expression.

Il existe des règles similaires pour appeler this(). Le compilateur dit "l'appel à ceci doit être la première déclaration dans le 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 arriverait?


491
2017-07-22 21:25


origine


Réponses:


La classe parente ' constructor doit être appelé avant la sous-classe ' constructor. Cela garantira que si vous appelez des méthodes sur la classe parente dans votre constructeur, la classe parente a déjà été configurée correctement.

Ce que vous essayez de faire, transmettre des arguments au super constructeur est parfaitement légal, il vous suffit de construire ces arguments en ligne comme vous le faites, ou de les transmettre à votre constructeur, puis de les transmettre à super:

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

Si le compilateur ne l'a pas appliqué, vous pouvez le faire:

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

Dans les cas où un parent classe a un constructeur par défaut l'appel à super est inséré pour vous automatiquement par le compiler. Puisque chaque classe de Java hérite de Object, le constructeur d'objets doit être appelé d'une manière ou d'une autre et il doit être exécuté en premier. L'insertion automatique de super () par le compilateur le permet. Appliquer super pour apparaître en premier, impose que les corps de constructeurs soient exécutés dans le bon ordre qui serait: Object -> Parent -> Child -> ChildOfChild -> SoOnSoForth


152
2017-07-22 21:27



J'ai trouvé un moyen de contourner cela en enchaînant les constructeurs et les méthodes statiques. Ce que je voulais faire ressemblait à 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;
  }
}

Donc, construisez fondamentalement un objet basé sur les paramètres du constructeur, stockez l'objet dans un membre, et passez aussi le résultat d'une méthode sur cet objet dans le constructeur de super. Rendre le membre final était également raisonnablement important car la nature de la classe est qu'elle est immuable. Notez qu'en réalité, la construction de Bar prend en fait quelques objets intermédiaires, donc il n'est pas réductible à un one-liner dans mon cas d'utilisation réel.

J'ai fini par le faire fonctionner quelque chose 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 juridique, et il accomplit la tâche d'exécuter plusieurs instructions avant d'appeler le super constructeur.


77
2017-07-15 13:47



Parce que le JLS le dit. Le JLS pourrait-il être modifié de manière compatible pour le permettre? Ouaip. Cependant, cela compliquerait la spécification du langage, qui est déjà plus que compliquée. Ce ne serait pas une chose très utile à faire et il y a des façons de contourner cela (appelez un autre constructeur avec le résultat d'une méthode this(fn())- la méthode est appelée avant l’autre constructeur, et donc aussi le super constructeur). Donc, le rapport puissance / poids du changement est défavorable.

Modifier Mars 2018: Dans le message Records: construction et validation Oracle suggère de supprimer cette restriction (mais contrairement à C #, this sera définitivement non affecté (DU) avant le chaînage du constructeur).

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


31
2017-07-22 21:38



Je suis à peu près certain (ceux qui connaissent bien le carillon de spécification Java) que cela vous empêche de (a) être autorisé à utiliser un objet partiellement construit, et (b) de forcer le constructeur de la classe parente à construire sur " objet.

Quelques exemples de "mauvaise" chose seraient:

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
    }        
}

13
2017-07-22 21:28



Vous avez demandé pourquoi, et les autres réponses, imo, ne dites pas vraiment pourquoi vous pouvez appeler le constructeur de votre super, mais seulement si c'est la toute première ligne. La raison en est que vous n'êtes pas vraiment appel le constructeur. En C ++, la syntaxe équivalente est

MySubClass: MyClass {

public:

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

};

Quand vous voyez la clause d'initialisation comme ça, avant l'accolade ouverte, vous savez qu'elle est spéciale. Il s'exécute avant que le reste du constructeur ne s'exécute et en fait avant que les variables membres soient initialisées. Ce n'est pas si différent pour Java. Il existe un moyen d'exécuter du code (d'autres constructeurs) avant que le constructeur ne démarre réellement, avant que les membres de la sous-classe ne soient initialisés. Et de cette façon est de mettre "l'appel" (par exemple super) sur la toute première ligne. (De façon que super ou this est un peu avant la première accolade ouverte, même si vous la tapez après, car elle sera exécutée avant que tout soit complètement construit.) Tout autre code après l'accolade ouverte (comme int c = a + b;) fait dire au compilateur "oh, ok, pas d’autres constructeurs, on peut tout initialiser". Donc, il se déroule et initialise votre super classe et vos membres et autres joyeusetés et commence à exécuter le code après l'accolade ouverte.

Si, quelques lignes plus tard, il répond à un code disant "oh ouais quand tu construis cet objet, voici les paramètres que je veux transmettre au constructeur pour la classe de base", c'est trop tard et ça ne le fait pas ne veut rien dire. Donc, vous obtenez une erreur de compilation.


9
2017-07-14 17:56



Simplement parce que c'est la philosophie de l'héritage. Et selon la spécification du langage Java, voici comment le corps du constructeur est défini:

ConstructorBody:         {ExplicitConstructorInvocationopter BlockStatementsopter }

La première déclaration d'un constructeur peut être:
-une invocation explicite d'un autre constructeur de la même classe (en utilisant le mot-clé "this") OU
-de la superclasse directe (en utilisant le mot-clé "super")

Si un corps de constructeur ne commence pas par un appel constructeur explicite et que le constructeur déclaré ne fait pas partie de la classe primordiale Object, alors le corps du constructeur commence implicitement par un invocation de constructeur de superclasse "super ();", invocation du constructeur de sa super-classe directe qui ne prend aucun argument. Et ainsi de suite ... il y aura toute une chaîne de constructeurs appelée tout le chemin du constructeur de l'objet; "Toutes les classes de la plate-forme Java sont des descendants d'objet". Cette chose s'appelle "Constructeur chaînage".

Maintenant pourquoi est-ce?
Et la raison pour laquelle Java a défini le ConstructorBody de cette façon, c'est qu'ils devaient maintenir la hiérarchie de l'objet. Rappelez-vous la définition de l'héritage; C'est prolonger une classe. Cela étant dit, vous ne pouvez pas étendre quelque chose qui n'existe pas. La base (la superclasse) doit d'abord être créée, ensuite vous pouvez la dériver (la sous-classe). C'est pourquoi ils les ont appelés classes parent et enfant; vous ne pouvez pas avoir d'enfant sans parent.

Au niveau technique, une sous-classe hérite de tous ses membres (champs, méthodes, classes imbriquées) de son parent. Et puisque les constructeurs ne sont pas membres (ils n'appartiennent pas aux objets, ils sont responsables de la création d'objets), ils ne sont pas hérités par les sous-classes, mais ils peuvent être invoqués. Et depuis au moment de la création de l'objet, UN seul constructeur est exécuté. Alors, comment pouvons-nous garantir la création de la superclasse lorsque vous créez l'objet de la sous-classe? Ainsi le concept de "constructeur chaînage"; Nous avons donc la possibilité d'invoquer d'autres constructeurs (c'est-à-dire super) depuis le constructeur actuel. Et Java a exigé que cette invocation soit la première ligne du constructeur de la sous-classe pour maintenir la hiérarchie et la garantir. Ils supposent que si vous ne créez pas explicitement l'objet parent FIRST (comme si vous l'avez oublié), ils le feront implicitement pour vous.

Cette vérification est effectuée lors de la compilation. Mais je ne suis pas sûr de ce qui arriverait à l'exécution, quel genre d'erreur d'exécution nous aurions, SI Java ne lance pas d'erreur de compilation quand nous essayons explicitement d'exécuter un constructeur de base depuis le constructeur d'une sous-classe au milieu de son corps et non de la toute première ligne ...


9
2017-09-29 09:31



Donc, cela ne vous empêche pas d'exécuter la logique avant l'appel à   super. Il est juste de vous empêcher d'exécuter la logique que vous ne pouvez pas adapter   en une seule expression.

En fait, vous pouvez exécuter une logique avec plusieurs expansions, il vous suffit d'envelopper votre code dans une fonction statique et de l'appeler dans l'instruction super.

En utilisant votre exemple:

public class MySubClassC extends MyClass {
    public MySubClassC(Object item) {
        // Create a list that contains the item, and pass the list to super
        super(createList(item));  // OK
    }

    private static List createList(item) {
        List list = new ArrayList();
        list.add(item);
        return list;
    }
}

5
2018-06-18 09:00



Je suis totalement d'accord, les restrictions sont trop fortes. Il n'est pas toujours possible d'utiliser une méthode d'aide statique (comme Tom Hawtin - tackline suggéré) ou de pousser tous les «calculs de pré-super (») en une seule expression dans le paramètre, par exemple:

class Sup {
    public Sup(final int x_) { 
        //cheap constructor 
    }
    public Sup(final Sup sup_) { 
        //expensive copy constructor 
    }
}

class Sub extends Sup {
    private int x;
    public Sub(final Sub aSub) {
        /* for aSub with aSub.x == 0, 
         * the expensive copy constructor is unnecessary:
         */

         /* if (aSub.x == 0) { 
          *    super(0);
          * } else {
          *    super(aSub);
          * } 
          * above gives error since if-construct before super() is not allowed.
          */

        /* super((aSub.x == 0) ? 0 : aSub); 
         * above gives error since the ?-operator's type is Object
         */

        super(aSub); // much slower :(  

        // further initialization of aSub
    }
}

L'utilisation d'une exception «objet non encore construit», comme l'a suggéré Carson Myers, aiderait, mais en vérifiant cela lors de la construction de chaque objet, cela ralentirait l'exécution. Je préférerais un compilateur Java qui fasse une meilleure différenciation (au lieu d'interdire par conséquent une if-statement mais en autorisant le? -operator dans le paramètre), même si cela complique la spécification du langage.


4
2017-07-14 17:38