35 votes

Pourquoi la délégation à un constructeur différent doit-elle se faire d'abord dans un constructeur Java ?

Dans un constructeur en Java, si vous voulez appeler un autre constructeur (ou un super constructeur), elle doit être la première ligne dans le constructeur. Je suppose que c'est parce que vous ne devriez pas être autorisé à modifier toutes les variables d'instance, avant que l'autre constructeur s'exécute. Mais pourquoi ne pouvez-vous pas avoir une déclaration avant le constructeur de délégation, afin de calculer la valeur complexe de 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 de contourner cette limitation.

Donc, je me demandais simplement:

  1. Est-il une bonne raison de cette limitation?
  2. Est-il prévu de permettre à l'avenir de cette versions de Java? (Ou Soleil a définitivement dit ce n'est pas qui va se passer?)


Pour un exemple de ce dont je parle, examiner certains code que j'ai écrit que j'ai donné dans ce StackOverflow réponse. Dans ce code, j'ai un BigFraction classe, qui a un BigInteger numérateur et un BigInteger dénominateur. "Canoniques" constructeur est l' BigFraction(BigInteger numerator, BigInteger denominator) formulaire. Pour tous les autres constructeurs, je viens de convertir les paramètres d'entrée à BigIntegers, et d'appeler la "canonique" du constructeur, parce que 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. Envisager le constructeur qui prend un BigDecimal:

  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 cela assez laid, mais il m'aide à éviter la duplication de code. Ce qui suit est ce que j'aimerais faire, mais il 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 à ce jour, aucune réponse n'a été fournie que j'en suis entièrement satisfait, mais je ne se soucient pas assez pour commencer une prime, donc je vais répondre à ma propre question (principalement pour se débarrasser de l'ennuyeux "avez-vous pensé de marquage accepté de répondre" message).

Les solutions ont été suggérées sont les suivantes:

  1. Statique de l'usine.
    • J'ai utilisé la classe dans beaucoup d'endroits, de sorte que le code serait briser si j'ai soudain eu débarrasser du public, les constructeurs et est allé avec de valueOf() les fonctions.
    • Il se sent comme une solution de contournement à une limitation. Je n'aurais pas d'autres avantages d'une usine parce que cela ne peut pas être sous-classé et parce que les valeurs communes ne sont pas mis en cache/internés.
  2. Private static "constructeur de l'aide" des méthodes.
    • Cela conduit à beaucoup de code de la météorisation.
    • Le code est moche parce que, dans certains cas, j'ai vraiment besoin de calculer le numérateur et le dénominateur dans le même temps, et je ne peux pas retourner plusieurs valeurs, à moins que je retourne un BigInteger[] ou une sorte de privé intérieur de la classe.

Le principal argument à l'encontre de cette fonctionnalité est que le compilateur aurait pour vérifier que vous n'avez pas l'utilisation de toutes les instances des variables ou des méthodes avant d'appeler la superconstructor, parce que l'objet est dans un état non valide. Je suis d'accord, mais je pense que ce serait plus facile de vérifier que celui qui s'assure que toutes les finales des variables d'instance sont toujours initialisés à chaque constructeur, n'importe quel chemin à travers le code est pris. L'autre argument est que vous simplement ne pouvez pas exécuter du code à l'avance, mais c'est manifestement faux, car le code pour calculer les paramètres de la superconstructor est arriver exécuté quelque part, donc il doit être permis à un bytecode niveau.

Maintenant, ce que j'aimerais voir, est une bonne raison pourquoi le compilateur ne pouvais pas me laisser prendre ce code:

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

Et de le réécrire comme ceci (le bytecode serait essentiellement identiques, je pense):

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

La seule vraie différence que je vois entre ces deux exemples est que le "tmp" de la variable lui permet d'être accessible après l'appel de this(tmp) dans le deuxième exemple. Alors peut-être une syntaxe particulière (similaire à l' static{} blocs pour l'initialisation de classe) devra ê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;
}

26voto

Je pense que plusieurs des réponses ici sont mauvais, car ils supposent que l'encapsulation est en quelque sorte rompu lors de l'appel à super() après l'invocation du code. Le fait est que le super peut le briser l'encapsulation de lui-même, parce que Java permet de substitution des méthodes dans le constructeur.

Tenir compte de 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 ne

new B();

le message imprimé est "null". C'est parce que le constructeur à partir d'Un accède à la non initialisée champ de B. Alors, franchement, il semble que si quelqu'un voulait le faire:

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

Alors que juste autant leur problème si ils font de la classe B ci-dessus. Dans les deux cas, le programmeur doit savoir comment les variables sont accessibles lors de la construction. Et étant donné que vous pouvez appeler super() ou this() avec toutes sortes d'expressions dans la liste des paramètres, il semble comme une restriction artificielle que vous ne pouvez pas calculer les expressions avant d'appeler un autre constructeur. Ne pas oublier que la restriction s'applique à la fois super() et this() lorsque je présume que vous savez comment faire pour ne pas briser votre propre encapsulation lors de l'appel d' this().

Mon verdict: Cette fonction est un bug du compilateur, peut-être à l'origine motivé par une bonne raison, mais dans sa forme actuelle, elle est une limitation artificielle avec aucun but.

14voto

Zach Scrivena Points 15052
<blockquote> <p>Je trouve cela assez laid, mais il aide moi éviter de dupliquer le code. Lla voici ce que j'aimerais faire, mais il est illégal à Java ...</p> <p>Vous pouvez également contourner cette limitation en utilisant une méthode d'usine statique qui renvoie un nouvel objet :</p><pre><code></code></pre><p>Alternativement, vous pouvez tricher en appelant une méthode statique privée pour faire les calculs pour votre constructeur:</p><pre><code></code></pre></blockquote>

10voto

Tmdean Points 4594

Les constructeurs doivent être appelés dans l'ordre, de la racine de la classe mère de la plupart de la classe dérivée. Vous ne pouvez pas exécuter n'importe quel code à l'avance dans le dérivé constructeur parce que, avant le constructeur parent est appelé, la trame de pile pour la dérivée constructeur n'a pas encore été alloué encore, parce que la dérivée constructeur n'a pas commencé l'exécution. Certes, la syntaxe de Java ne pas faire de ce fait claire.

Edit: Pour résumer, quand un constructeur de classe dérivée est "l'exécution" avant le présent appel, les points suivants s'appliquent.

  1. Variables de membre ne peut pas être touché, parce qu'ils ne sont pas valides avant de la base de les classes sont construites.
  2. Les Arguments sont en lecture seule, parce que le frame de pile n'a pas été attribué.
  3. Les variables locales ne sont pas accessibles, car la trame de pile n'a pas été attribué.

Vous pouvez accéder à des arguments et des variables locales si vous avez alloué les constructeurs de la pile d'images dans l'ordre inverse, de classes dérivées pour les classes de base, mais il faudrait que tous les cadres à être actifs en même temps, de perdre la mémoire pour chaque objet de la construction afin de permettre les rares cas de code qui veut toucher les variables locales avant les classes de base sont construits.

7voto

Esko Luontola Points 53877

"Ma conjecture est que, jusqu'à ce qu'un constructeur a été appelée pour chaque niveau de la heierarchy, l'objet est dans un état non valide. Il est dangereux pour la JVM pour exécuter quoi que ce soit jusqu'à ce qu'il a été entièrement construit."

En fait, il est possible de construire des objets en Java sans appel de chaque constructeur dans la hiérarchie, mais pas avec l' new mot-clé.

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

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

Ou si vous êtes si incliné, vous pouvez générer du bytecode vous-même (avec l'ASM ou similaire). Au niveau du bytecode, new Foo() compile de 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 de sa super-classe. Sinon, la JVM du chargeur de classe, une exception sera levée lors du chargement de la classe, en se plaignant qu'il n'y a pas d'appel à la super().

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