Pourquoi la délégation à un constructeur différent doit-elle se produire en premier dans un constructeur Java?

Dans un constructeur en Java, si vous voulez appeler un autre constructeur (ou un super constructeur), il doit s'agir de la première ligne du constructeur. Je suppose que c'est parce que vous ne devriez pas être autorisé à modifier les variables d'instance avant que l'autre constructeur ne s'exécute. Mais pourquoi ne pouvez-vous pas avoir des instructions avant la délégation du constructeur, afin de calculer la valeur complexe à l'autre fonction? Je ne peux pas penser à une bonne raison, et j'ai frappé quelques cas réels où j'ai écrit certains laid code pour contourner cette limitation.

Donc je me demande juste:

  1. y a-t-il une bonne raison à cette limitation?
  2. existe-t-il des plans pour permettre cela dans les futures versions Java? (Ou Sun a-t-il définitivement dit que cela ne se produirait pas?)

Pour un exemple de ce dont je parle, considérons un code que j'ai écrit et que j'ai donné dans cette réponse StackOverflow . Dans ce code, j'ai une classe BigFraction, qui a un numérateur BigInteger et un dénominateur BigInteger. Le constructeur "canonique" est la forme BigFraction(BigInteger numerator, BigInteger denominator). Pour tous les autres constructeurs, Je convertit simplement les paramètres d'entrée en BigIntegers et appelle le constructeur "canonique", car je ne veux pas dupliquer tout le travail.

, Dans certains cas, c'est facile; par exemple, le constructeur qui prend deux longs est trivial:

  public BigFraction(long numerator, long denominator)
  {
    this(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator));
  }

, Mais dans d'autres cas, c'est plus difficile. Considérons le constructeur qui prend un Granddecimal:

  public BigFraction(BigDecimal d)
  {
    this(d.scale() < 0 ? d.unscaledValue().multiply(BigInteger.TEN.pow(-d.scale())) : d.unscaledValue(),
         d.scale() < 0 ? BigInteger.ONE                                             : BigInteger.TEN.pow(d.scale()));
  }

Je trouve ça assez laid, mais cela m'aide à éviter la duplication du code. Ce qui suit est ce que je voudrais faire, mais c'est illégal en Java:

  public BigFraction(BigDecimal d)
  {
    BigInteger numerator = null;
    BigInteger denominator = null;
    if(d.scale() < 0)
    {
      numerator = d.unscaledValue().multiply(BigInteger.TEN.pow(-d.scale()));
      denominator = BigInteger.ONE;
    }
    else
    {
      numerator = d.unscaledValue();
      denominator = BigInteger.TEN.pow(d.scale());
    }
    this(numerator, denominator);
  }

Mise à Jour

Il y a eu de bonnes réponses, mais jusqu'à présent, aucune réponse n'a été fournie dont je suis complètement satisfait, mais je ne me soucie pas assez de commencer une prime, donc je réponds à ma propre question (principalement pour me débarrasser de ce message ennuyeux "avez-vous envisagé de marquer une réponse acceptée").

Solutions de contournement qui ont été les suggestions sont:

  1. usine statique.
    • j'ai utilisé la classe dans beaucoup d'endroits, de sorte que le code se casserait si je me débarrassais soudainement des constructeurs publics et allais avec les fonctions valueOf ().
    • cela ressemble à une solution de contournement à une limitation. Je n'obtiendrais aucun autre avantage d'une usine car cela ne peut pas être sous-classé et parce que les valeurs communes ne sont pas mises en cache / internées.
  2. méthodes statiques privées "constructeur helper".
    • Ce conduit à beaucoup de ballonnement de code.
    • le code devient moche parce que dans certains cas, j'ai vraiment besoin de calculer à la fois le numérateur et le dénominateur en même temps, et je ne peux pas retourner plusieurs valeurs à moins de retourner un BigInteger[] ou une sorte de classe interne privée.

L'argument principal contre cette fonctionnalité est que le compilateur devrait vérifier que vous n'avez pas utilisé de variables ou de méthodes d'instance avant d'appeler le superconstructeur, car l'objet serait dans un état non valide. Je suis d'accord, mais je pense que ce serait une vérification plus facile que celle qui s'assure que toutes les variables d'instance finales sont toujours initialisées dans chaque constructeur, quel que soit le chemin parcouru par le code. L'autre argument est que vous ne pouvez tout simplement pas exécuter du code à l'avance, mais c'est clairement faux car le code pour calculer les paramètres du superconstructeur est exécuté quelque part, donc il doit être autorisé au niveau du bytecode.

Maintenant, ce que j'aimerais pour voir, est-ce que bonne raison pour laquelle le compilateur ne pouvait pas me laisser prendre ce code:

public MyClass(String s) {
  this(Integer.parseInt(s));
}
public MyClass(int i) {
  this.i = i;
}

Et réécrivez-le comme ceci (le bytecode serait fondamentalement identique, je pense):

public MyClass(String s) {
  int tmp = Integer.parseInt(s);
  this(tmp);
}
public MyClass(int i) {
  this.i = i;
}

La seule différence réelle que je vois entre ces deux exemples est que la portée de la variable "tmp " lui permet d'être accessible après avoir appelé this(tmp) dans le second exemple. Donc, peut-être qu'une syntaxe spéciale (similaire aux blocs static{} pour l'initialisation de classe) devrait être introduite:

public MyClass(String s) {
  //"init{}" is a hypothetical syntax where there is no access to instance
  //variables/methods, and which must end with a call to another constructor
  //(using either "this(...)" or "super(...)")
  init {
    int tmp = Integer.parseInt(s);
    this(tmp);
  }
}
public MyClass(int i) {
  this.i = i;
}
34
demandé sur Community 2009-02-03 22:52:06

8 réponses

Je trouve cela assez laid, mais ça aide moi éviter de dupliquer le code. Le voici ce que j'aimerais faire, mais c'est illégal à Java ...

Vous pouvez également contourner cette limitation en utilisant une statique usine méthode qui renvoie un nouvel objet:

public static BigFraction valueOf(BigDecimal d)
{
    // computate numerator and denominator from d

    return new BigFraction(numerator, denominator);
}

Alternativement, vous pouvez tricher en appelant une méthode statique privée pour faire les calculs pour votre constructeur:

public BigFraction(BigDecimal d)
{
    this(computeNumerator(d), computeDenominator(d));
}

private static BigInteger computeNumerator(BigDecimal d) { ... }
private static BigInteger computeDenominator(BigDecimal d) { ... }        
13
répondu Zach Scrivena 2009-02-03 21:11:32

Je pense que plusieurs des réponses ici sont fausses car elles supposent que l'encapsulation est en quelque sorte cassée lors de l'appel de super() après avoir invoqué du code. Le fait est que le super peut réellement casser l'encapsulation elle-même, car Java permet de surcharger les méthodes dans le constructeur.

Considérez ces classes:

class A {
  protected int i;
  public void print() { System.out.println("Hello"); }
  public A() { i = 13; print(); }
}

class B extends A {
  private String msg;
  public void print() { System.out.println(msg); }
  public B(String msg) { super(); this.msg = msg; }
}

Si vous le faites

new B("Wubba lubba dub dub");

Le message imprimé est "null". C'est parce que le constructeur de A accède au champ non initialisé de B. donc franchement semble que si quelqu'un voulait faire ceci:

class C extends A {
  public C() { 
    System.out.println(i); // i not yet initialized
    super();
  }
} 

Alors c'est tout autant leur problème que s'ils font la Classe B ci-dessus. Dans les deux cas, le programmeur doit savoir comment les variables sont accessibles pendant la construction. Et étant donné que vous pouvez appeler super() ou this() avec toutes sortes d'expressions dans la liste des paramètres, cela semble être une restriction artificielle que vous ne pouvez pas calculer d'expressions avant d'appeler l'autre constructeur. Sans oublier que la restriction s'applique aux deux super() et {[4] } quand vous savez probablement comment ne pas casser votre propre encapsulation en appelant this().

Mon verdict: cette fonctionnalité est un bug dans le compilateur, peut-être à l'origine motivé par une bonne raison, mais dans sa forme actuelle c'est une limitation artificielle sans but.

25
répondu Mr. Shiny and New 安宇 2016-01-11 14:30:30

Les constructeurs doivent être appelés dans l'ordre, de la classe parent racine à la classe la plus dérivée. Vous ne pouvez pas exécuter de code au préalable dans le constructeur dérivé car avant l'appel du constructeur parent, le cadre de pile du constructeur dérivé n'a même pas encore été alloué, car le constructeur dérivé n'a pas commencé à s'exécuter. Certes, la syntaxe pour Java ne rend pas ce fait clair.

Edit: pour résumer, quand un constructeur de classe dérivé "s'exécute" avant le présent appel, les points suivants s'appliquent.

  1. les variables membres ne peuvent pas être touchées, car elles ne sont pas valides avant la base les classes sont construites.
  2. les Arguments sont en lecture seule, car le cadre de la pile n'a pas été alloué.
  3. les variables locales ne sont pas accessibles, car le cadre de la pile n'a pas été alloué.

Vous pouvez accéder aux arguments et aux variables locales si vous avez alloué les trames de pile des constructeurs dans l'ordre inverse, des classes dérivées à classes de base, mais cela nécessiterait que toutes les images soient actives en même temps, gaspillant de la mémoire pour chaque construction d'objet pour permettre le cas rare de code qui veut toucher les variables locales avant que les classes de base soient construites.

11
répondu Tmdean 2009-02-03 21:22:23

" ma conjecture est que, jusqu'à ce qu'un constructeur ait été appelé pour chaque niveau de la heierarchy, l'objet est dans un état invalide. Il est dangereux pour la JVM d'exécuter quoi que ce soit sur elle jusqu'à ce qu'elle ait été complètement construite."

En fait, il est possible de construire des objets en Java sans appeler tous les constructeurs de la hiérarchie, mais pas avec le mot clé new.

Par exemple, lorsque la sérialisation de Java construit un objet pendant désérialisation, il appelle le constructeur de la première classe non sérialisable dans la hiérarchie. Ainsi, lorsque java.util.HashMap est désérialisé, d'abord un java.util.L'instance HashMap est allouée, puis le constructeur de sa première superclasse java non sérialisable.util.AbstractMap est appelé (qui à son tour appelle java.lang.Constructeur de l'objet).

Vous pouvez également utiliser la bibliothèque Objenesis pour instancier des objets sans appeler le constructeur.

, Ou si vous êtes si incliné, vous peut générer le bytecode vous-même (avec ASM ou similaire). Au niveau du bytecode, new Foo() compile deux instructions:

NEW Foo
INVOKESPECIAL Foo.<init> ()V

, Si vous voulez éviter d'appeler le constructeur de Foo, vous pouvez modifier la deuxième commande, par exemple:

NEW Foo
INVOKESPECIAL java/lang/Object.<init> ()V

Mais même alors, le constructeur de Foo doit contenir un appel à sa superclasse. Sinon, le chargeur de classe de la JVM lancera une exception lors du chargement de la classe, se plaignant qu'il n'y a pas d'appel à super().

6
répondu Esko Luontola 2009-02-03 21:49:05

Permettre au code de ne pas appeler le super constructeur casse d'abord l'encapsulation - l'idée que vous pouvez écrire du code et pouvoir prouver que peu importe ce que fait quelqu'un d'autre - l'étendre, l'invoquer, l'instancier - il sera toujours dans un état valide.

IOW: ce n'est pas une exigence JVM en tant que telle, mais une exigence Comp Sci. Et important.

Pour résoudre votre problème, incidemment, vous utilisez des méthodes statiques privées - elles ne dépendent d'aucune exemple:

public BigFraction(BigDecimal d)
{
  this(appropriateInitializationNumeratorFor(d),
       appropriateInitializationDenominatorFor(d));
}

private static appropriateInitializationNumeratorFor(BigDecimal d)
{
  if(d.scale() < 0)
  {
    return d.unscaledValue().multiply(BigInteger.TEN.pow(-d.scale()));
  }
  else
  {
    return d.unscaledValue();
  }
}

Si vous n'aimez pas avoir des méthodes séparées (beaucoup de logique commune que vous ne voulez exécuter qu'une seule fois, par exemple), ayez une méthode qui renvoie une petite classe interne statique privée qui est utilisée pour appeler un constructeur privé.

1
répondu 2009-02-04 14:39:51

Mon guess est que, jusqu'à ce qu'un constructeur ait été appelé pour chaque niveau de la heierarchy, l'objet est dans un état invalide. Il est dangereux pour la JVM d'exécuter quoi que ce soit sur elle jusqu'à ce qu'elle ait été complètement construite.

0
répondu 2009-02-03 19:57:11

Eh bien, le problème est que java ne peut pas détecter les 'instructions' que vous allez mettre avant le super appel. Par exemple, vous pouvez vous référer à des variables membres qui ne sont pas encore initialisées. Donc, je ne pense pas que java supportera jamais cela. Maintenant, il existe de nombreuses façons de contourner ce problème, par exemple en utilisant des méthodes d'usine ou de modèle.

0
répondu amit 2015-05-26 15:10:19

Regarde par là.

Disons qu'un objet est composé de 10 pièces.

1,2,3,4,5,6,7,8,9,10

Ok?

De 1 à 9 sont dans la super classe, partie # 10 est votre ajout.

Simple ne peut pas ajouter la 10e partie tant que les 9 précédentes ne sont pas terminées.

C'est ça.

Si de 1-6 proviennent d'une autre super classe aussi bien, la chose est qu'un seul objet est créé dans une séquence spécifique, c'est la façon dont est conçu.

Sur bien sûr, la vraie raison est beaucoup plus complexe que cela, mais je pense que cela répondrait à peu près à la question.

En ce qui concerne les alternatives, je pense qu'il y a beaucoup déjà posté ici.

-3
répondu OscarRyz 2009-02-12 06:17:57