40 Stimmen

Warum muss in einem Java-Konstruktor zuerst die Delegation an einen anderen Konstruktor erfolgen?

Wenn Sie in einem Konstruktor in Java einen anderen Konstruktor (oder einen Superkonstruktor) aufrufen wollen, muss dies in der ersten Zeile des Konstruktors stehen. Ich nehme an, das liegt daran, dass man keine Instanzvariablen ändern darf, bevor der andere Konstruktor läuft. Aber warum kann man keine Anweisungen vor der Delegation des Konstruktors haben, um den komplexen Wert für die andere Funktion zu berechnen? Mir fällt kein guter Grund ein, und ich bin schon auf einige reale Fälle gestoßen, in denen ich hässlichen Code geschrieben habe, um diese Einschränkung zu umgehen.

Ich frage mich nur:

  1. Gibt es einen guten Grund für diese Einschränkung?
  2. Gibt es Pläne, dies in zukünftigen Java-Versionen zu ermöglichen? (Oder hat Sun definitiv gesagt, dass dies nicht passieren wird?)

Ein Beispiel dafür, wovon ich spreche, ist ein von mir geschriebener Code, den ich in diese StackOverflow-Antwort . In diesem Code habe ich eine BigFraction-Klasse, die einen BigInteger-Zähler und einen BigInteger-Nenner hat. Der "kanonische" Konstruktor ist der BigFraction(BigInteger numerator, BigInteger denominator) Form. Für alle anderen Konstruktoren konvertiere ich einfach die Eingabeparameter in BigInteger und rufe den "kanonischen" Konstruktor auf, weil ich die ganze Arbeit nicht duplizieren möchte.

In einigen Fällen ist dies einfach; zum Beispiel nimmt der Konstruktor, der zwei long s ist trivial:

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

Aber in anderen Fällen ist es schwieriger. Betrachten wir den Konstruktor, der eine 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()));
  }

Ich finde das ziemlich hässlich, aber es hilft mir, doppelten Code zu vermeiden. Das Folgende würde ich gerne tun, aber es ist in Java illegal:

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

Update

Es gab gute Antworten, aber bis jetzt wurde noch keine Antwort gegeben, mit der ich vollkommen zufrieden bin, aber es ist mir nicht wichtig genug, um ein Kopfgeld auszusetzen, also beantworte ich meine eigene Frage (hauptsächlich, um die lästige Nachricht "Haben Sie daran gedacht, eine akzeptierte Antwort zu markieren" loszuwerden).

Folgende Abhilfemaßnahmen wurden vorgeschlagen:

  1. Statische Fabrik.
    • Ich habe die Klasse an vielen Stellen verwendet, so dass der Code kaputt gehen würde, wenn ich plötzlich die öffentlichen Konstruktoren loswerden und mit valueOf()-Funktionen arbeiten würde.
    • Es wirkt wie eine Umgehung einer Einschränkung. Ich würde keine weiteren Vorteile einer Fabrik erhalten, weil diese nicht unterklassifiziert werden kann und weil gemeinsame Werte nicht zwischengespeichert/verwendet werden.
  2. Private statische "Konstruktor-Helfer"-Methoden.
    • Dies führt zu einer starken Aufblähung des Codes.
    • Der Code wird hässlich, weil ich in einigen Fällen wirklich Zähler und Nenner gleichzeitig berechnen muss, und ich kann nicht mehrere Werte zurückgeben, es sei denn, ich gebe eine BigInteger[] oder eine Art private innere Klasse.

Das Hauptargument gegen diese Funktionalität ist, dass der Compiler vor dem Aufruf des Superkonstruktors prüfen müsste, dass Sie keine Instanzvariablen oder -methoden verwendet haben, da sich das Objekt in einem ungültigen Zustand befinden würde. Dem stimme ich zu, aber ich denke, dies wäre eine einfachere Prüfung als die, die sicherstellt, dass alle finalen Instanzvariablen immer in jedem Konstruktor initialisiert werden, egal welchen Weg man durch den Code nimmt. Das andere Argument ist, dass man einfach keinen Code vorher ausführen kann, aber das ist eindeutig falsch, weil der Code zum Berechnen der Parameter für den Superkonstruktor ausgeführt wird irgendwo also muss es auf Bytecode-Ebene erlaubt sein.

Was ich gerne sehen würde, sind einige gut Grund, warum der Compiler mich diesen Code nicht nehmen lassen konnte:

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

Und schreiben Sie es so um (der Bytecode wäre im Grunde identisch, denke ich):

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

Der einzige wirkliche Unterschied, den ich zwischen diesen beiden Beispielen sehe, ist, dass die " tmp "Der Gültigkeitsbereich der Variablen ermöglicht den Zugriff auf sie nach dem Aufruf von this(tmp) im zweiten Beispiel. Daher könnte eine spezielle Syntax (ähnlich wie bei static{} Blöcke für die Klasseninitialisierung) müssten eingeführt werden:

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

28voto

Ich denke, dass einige der Antworten hier falsch sind, weil sie davon ausgehen, dass die Kapselung irgendwie gebrochen wird, wenn man super() aufruft, nachdem man einen Code aufgerufen hat. Tatsache ist, dass der Super()-Aufruf selbst die Kapselung durchbrechen kann, weil Java das Überschreiben von Methoden im Konstruktor erlaubt.

Betrachten Sie diese Klassen:

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

Wenn Sie das tun

new B("Wubba lubba dub dub");

ist die ausgedruckte Meldung "null". Das liegt daran, dass der Konstruktor von A auf das nicht initialisierte Feld von B zugreift:

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

Dann ist das genauso ihr Problem, wie wenn sie in die Klasse B aufsteigen. In beiden Fällen muss der Programmierer wissen, wie auf die Variablen während der Konstruktion zugegriffen wird. Und wenn man bedenkt, dass man super() o this() mit allen möglichen Ausdrücken in der Parameterliste, scheint es eine künstliche Einschränkung zu sein, dass man keine Ausdrücke berechnen kann, bevor man den anderen Konstruktor aufruft. Ganz zu schweigen davon, dass diese Einschränkung sowohl für super() y this() wenn Sie vermutlich wissen, wie Sie Ihre eigene Verkapselung beim Aufruf von this() .

Meine Meinung: Diese Funktion ist ein Fehler im Compiler, der vielleicht ursprünglich einen guten Grund hatte, aber in seiner jetzigen Form ist es eine künstliche Einschränkung ohne Zweck.

16voto

Zach Scrivena Punkte 28381

Ich finde das ziemlich hässlich, aber es hilft doppelten Code zu vermeiden. Die Folgendes würde ich gerne tun, aber es ist in Java illegal ...

Sie können diese Einschränkung auch umgehen, indem Sie eine statische Fabrikmethode verwenden, die ein neues Objekt zurückgibt:

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

    return new BigFraction(numerator, denominator);
}

Alternativ können Sie auch eine private statische Methode aufrufen, um die Berechnungen für Ihren Konstruktor durchzuführen:

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

private static BigInteger computeNumerator(BigDecimal d) { ... }
private static BigInteger computeDenominator(BigDecimal d) { ... }

10voto

Tmdean Punkte 8928

Die Konstruktoren müssen der Reihe nach aufgerufen werden, von der Root-Elternklasse bis zur am meisten abgeleiteten Klasse. Im abgeleiteten Konstruktor kann vorher kein Code ausgeführt werden, denn bevor der übergeordnete Konstruktor aufgerufen wird, ist der Stack-Frame für den abgeleiteten Konstruktor noch nicht einmal alloziert worden, weil der abgeleitete Konstruktor noch nicht mit der Ausführung begonnen hat. Zugegeben, die Syntax von Java macht diese Tatsache nicht deutlich.

Edit: Zusammenfassend lässt sich sagen, dass, wenn ein Konstruktor einer abgeleiteten Klasse vor dem this()-Aufruf "ausgeführt" wird, die folgenden Punkte gelten.

  1. Mitgliedsvariablen können nicht berührt werden, da sie ungültig sind, bevor Basisklassen Klassen konstruiert werden.
  2. Die Argumente sind schreibgeschützt, da der Stapelrahmen noch nicht zugewiesen wurde.
  3. Auf lokale Variablen kann nicht zugegriffen werden, da der Stack-Frame noch nicht zugewiesen wurde.

Sie können auf Argumente und lokale Variablen zugreifen, wenn Sie die Stack-Frames der Konstruktoren in umgekehrter Reihenfolge zuweisen, von abgeleiteten Klassen zu Basisklassen, aber dies würde erfordern, dass alle Frames gleichzeitig aktiv sind, was Speicher für jede Objektkonstruktion verschwendet, um den seltenen Fall von Code zu ermöglichen, der auf lokale Variablen zugreifen will, bevor Basisklassen konstruiert werden.

8voto

Esko Luontola Punkte 71758

"Ich vermute, dass sich das Objekt in einem ungültigen Zustand befindet, bis ein Konstruktor für jede Ebene der Hierarchie aufgerufen wurde. Es ist für die JVM unsicher, irgendetwas darauf auszuführen, bevor es nicht vollständig konstruiert wurde."

Eigentlich ist es es möglich, Objekte in Java zu konstruieren, ohne かくはん Konstruktor in der Hierarchie, allerdings nicht mit dem new Stichwort.

Wenn zum Beispiel die Java-Serialisierung ein Objekt während der Deserialisierung konstruiert, ruft sie den Konstruktor der ersten nicht serialisierbaren Klasse in der Hierarchie auf. Wenn also java.util.HashMap deserialisiert wird, wird zunächst eine java.util.HashMap-Instanz zugewiesen und dann der Konstruktor der ersten nicht-serialisierbaren Oberklasse java.util.AbstractMap aufgerufen (die wiederum den Konstruktor von java.lang.Object aufruft).

Sie können auch die Objenesis Bibliothek, um Objekte ohne Aufruf des Konstruktors zu instanziieren.

Wenn Sie es wünschen, können Sie den Bytecode auch selbst generieren (mit ASM oder ähnlich). Auf der Ebene des Bytecodes, new Foo() kompiliert zu zwei Anweisungen:

NEW Foo
INVOKESPECIAL Foo.<init> ()V

Wenn Sie den Aufruf des Konstruktors von Foo vermeiden wollen, können Sie zum Beispiel den zweiten Befehl ändern:

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

Aber selbst dann muss der Konstruktor von Foo einen Aufruf an seine Oberklasse enthalten. Andernfalls wird der Klassenlader der JVM beim Laden der Klasse eine Ausnahme auslösen und sich darüber beschweren, dass es keinen Aufruf an super() .

1voto

Wenn man dem Code erlaubt, den Superkonstruktor nicht zuerst aufzurufen, bricht man die Kapselung auf - die Idee, dass man Code schreiben und in der Lage sein kann beweisen dass es sich immer in einem gültigen Zustand befindet, ganz gleich, was jemand anderes tut - es erweitern, aufrufen, instanziieren -.

Das heißt, es ist keine JVM-Anforderung als solche, sondern eine Comp Sci-Anforderung. Und zwar eine wichtige.

Um Ihr Problem zu lösen, verwenden Sie übrigens private statische Methoden - sie sind von keiner Instanz abhängig:

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();
  }
}

Wenn Sie keine separaten Methoden haben möchten (z. B. eine Menge allgemeiner Logik, die Sie nur einmal ausführen möchten), können Sie eine Methode verwenden, die eine private kleine statische innere Klasse zurückgibt, die zum Aufrufen eines privaten Konstruktors verwendet wird.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X