725 Stimmen

Warum müssen this() und super() die erste Anweisung in einem Konstruktor sein?

Java schreibt vor, dass der Aufruf von this() oder super() in einem Konstruktor die erste Anweisung sein muss. Warum?

Zum Beispiel:

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

Der Sun-Compiler sagt: "Der Aufruf von super muss die erste Anweisung im Konstruktor sein". Der Eclipse-Compiler sagt: "Constructor call must be the first statement in a constructor".

Sie können dies jedoch umgehen, indem Sie den Code ein wenig umgestalten:

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

Hier ein weiteres Beispiel:

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

Es ist also Sie nicht daran hindern, die Logik auszuführen vor dem Anruf beim Hausmeister. Es hält Sie nur davon ab, Logik auszuführen, die Sie nicht in einen einzigen Ausdruck packen können.

Es gibt ähnliche Regeln für den Aufruf this() . Der Compiler sagt: "Der Aufruf von this muss die erste Anweisung im Konstruktor sein".

Warum hat der Compiler diese Einschränkungen? Können Sie ein Codebeispiel nennen, bei dem etwas Schlimmes passieren würde, wenn der Compiler diese Einschränkung nicht hätte?

9 Stimmen

Eine gute Frage. Ich habe ein ähnliches Projekt gestartet valjok.blogspot.com/2012/09/ und programmers.exchange, wo ich zeige, dass es Fälle gibt, in denen Unterfelder vor dem super() initialisiert werden müssen. Die Funktion erhöht also die Komplexität, wobei nicht klar ist, ob die positiven Auswirkungen in Bezug auf die "Codesicherheit" die negativen überwiegen. Ja, es gibt negative Konsequenzen, wenn super immer zuerst kommt. Überraschenderweise hat dies niemand erwähnt. Ich denke, dass dies eine konzeptionelle Angelegenheit ist und in programmers.exchange gefragt werden muss.

60 Stimmen

Das Schlimmste daran ist, dass dies eine reine Java-Beschränkung ist. Auf der Bytecode-Ebene gibt es keine solche Einschränkung.

2 Stimmen

Nun, es wäre unmöglich, diese Einschränkung auf der Bytecode-Ebene zu haben - alle Beispiele in diesem Beitrag würden eine solche Einschränkung verletzen, auch die, die die gesamte Logik in einen einzigen Ausdruck packen.

218voto

anio Punkte 8423

Der Konstruktor der übergeordneten Klasse muss vor dem Konstruktor der Unterklasse aufgerufen werden. Dadurch wird sichergestellt, dass, wenn Sie in Ihrem Konstruktor Methoden der übergeordneten Klasse aufrufen, die übergeordnete Klasse bereits korrekt eingerichtet wurde.

Was Sie versuchen zu tun, übergeben Sie Args an den Superkonstruktor ist völlig legal, Sie müssen nur diese Args inline konstruieren, wie Sie tun, oder übergeben Sie in Ihrem Konstruktor und dann übergeben Sie an super :

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

Wenn der Compiler dies nicht erzwingen würde, könnten Sie dies tun:

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

In Fällen, in denen eine übergeordnete Klasse einen Standardkonstruktor hat, wird der Aufruf von super automatisch vom Compiler eingefügt. Da jede Klasse in Java erbt von Object Der Konstruktor des Objekts muss irgendwie aufgerufen und zuerst ausgeführt werden. Die automatische Einfügung von super() durch den Compiler ermöglicht dies. Dadurch, dass super() zuerst erscheinen muss, wird erzwungen, dass die Konstruktorkörper in der richtigen Reihenfolge ausgeführt werden, nämlich Objekt -> Parent -> Child -> ChildOfChild -> SoOnSoForth

1 Stimmen

Zugestimmt. Man kann nichts mit dem Objekt machen, bis sein Elternteil konstruiert ist. Sie können mit anderen Dingen arbeiten, aber nicht mit dem Objekt

0 Stimmen

Was ist mit anonymen Initialisierungsblöcken? Lesen Sie meine Antwort.

0 Stimmen

@Savvas Der anonyme Intialisierer verstößt nicht gegen die Reihenfolge der Konstruktoraufrufe, daher ist er in Ordnung. Das übergeordnete Element wird zuerst konstruiert, und dann wird der anonyme Initialisierungscode ausgeführt, also ist es sicher. Die Konstruktorkörper müssen in der folgenden Reihenfolge ausgeführt werden: Objekt-Konstruktorkörper -> Eltern-Konstruktorkörper -> Kind-Konstruktorkörper Ich glaube, um diese Reihenfolge zu erzwingen, muss der Aufruf von super zuerst erfolgen, so dass der Objekt-Konstruktorkörper zuerst ausgeführt wird, da er keinen super hat, und dann werden die anderen Konstruktorkörper abgewickelt und ausgeführt.

113voto

pendor Punkte 1730

Ich habe einen Weg gefunden, dies durch Verkettung von Konstruktoren und statischen Methoden zu umgehen. Was ich tun wollte, sah in etwa so aus:

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

Man konstruiert also ein Objekt auf der Grundlage von Konstruktorparametern, speichert das Objekt in einem Member und übergibt das Ergebnis einer Methode für dieses Objekt an den Konstruktor von super. Es war auch ziemlich wichtig, das Mitglied final zu machen, da die Klasse von Natur aus unveränderlich ist. Beachten Sie, dass die Konstruktion von Bar tatsächlich einige Zwischenobjekte erfordert, so dass sie in meinem tatsächlichen Anwendungsfall nicht auf einen Einzeiler reduziert werden kann.

Am Ende habe ich es etwa so gemacht:

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

Legaler Code, der die Aufgabe erfüllt, vor dem Aufruf des Superkonstruktors mehrere Anweisungen auszuführen.

0 Stimmen

Diese Technik kann erweitert werden. Wenn super viele Parameter benötigt oder Sie gleichzeitig andere Felder setzen müssen, erstellen Sie eine statische innere Klasse, die alle Variablen enthält, und verwenden Sie sie, um Daten von der statischen Methode an den Ein-Arg-Konstruktor zu übergeben.

25 Stimmen

FYI, sehr oft, wenn es scheint, dass Sie tun müssen, Logik vor dem Aufruf super sind Sie besser dran, wenn Sie die Komposition statt der Vererbung verwenden.

0 Stimmen

Es hat ein bisschen gedauert, bis ich Ihr Konzept verstanden habe. Also grundsätzlich erstellt man eine statische Methode und fügt sie in den Konstruktor ein.

60voto

Tom Hawtin - tackline Punkte 142461

Weil die JLS das sagt. Könnte das JLS in einer kompatiblen Weise geändert werden, um dies zu ermöglichen? Jawohl.

Allerdings würde dies die Sprachspezifikation verkomplizieren, die bereits mehr als kompliziert genug ist. Es wäre nicht sehr nützlich und es gibt Möglichkeiten, es zu umgehen (Aufruf eines anderen Konstruktors mit dem Ergebnis einer statischen Methode oder eines Lambda-Ausdrucks this(fn()) - die Methode wird vor dem anderen Konstruktor und damit auch vor dem Superkonstruktor aufgerufen). Das Verhältnis zwischen Leistung und Gewicht der Änderung ist also ungünstig.

Beachten Sie, dass diese Regel allein die Verwendung von Feldern nicht verhindert, bevor die Superklasse ihre Konstruktion abgeschlossen hat.

Betrachten Sie diese illegalen Beispiele.

super(this.x = 5);

super(this.fn());

super(fn());

super(x);

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

Dieses Beispiel ist legal, aber "falsch".

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

Im obigen Beispiel, wenn MyDerived.fn erforderlichen Argumente aus dem MyDerived Konstruktor müssten sie mit einer ThreadLocal . ;(

Übrigens: Seit Java 1.4 enthält das synthetische Feld, das die äußere this zugewiesen wird, bevor der Superkonstruktor der inneren Klassen aufgerufen wird. Dies verursachte merkwürdige NullPointerException Ereignisse in Code, der für frühere Versionen kompiliert wurde.

Beachten Sie auch, dass bei einer unsicheren Veröffentlichung die Konstruktion von anderen Threads neu geordnet werden kann, wenn keine Vorsichtsmaßnahmen getroffen werden.

Bearbeiten März 2018: In der Nachricht Aufzeichnungen: Aufbau und Validierung Oracle schlägt vor, diese Einschränkung zu entfernen (aber im Gegensatz zu C#, this wird sein definitiv nicht zugewiesen (DU) vor der Verkettung der Konstruktoren).

Historisch gesehen, muss this() oder super() in einem Konstruktor an erster Stelle stehen. Diese Beschränkung war nie beliebt und wurde als willkürlich empfunden. Es gab eine Reihe von subtilen Gründen, einschließlich der Überprüfung von invokespecial, die zu dieser Einschränkung beigetragen haben. Im Laufe der Jahre haben wir diese auf der VM-Ebene angegangen, bis zu dem Punkt, an dem es dass es sinnvoll ist, diese Einschränkung aufzuheben, nicht nur für Records, sondern für alle Konstruktoren.

1 Stimmen

Nur zu klären: die fn(), die Sie in Ihrem Beispiel verwendet sollte eine statische Methode sein, richtig?

10 Stimmen

+1 für die Erwähnung, dass dies eine reine JLS-Beschränkung ist. Auf der Bytecode-Ebene können Sie andere Dinge tun, bevor Sie einen Konstruktor aufrufen.

3 Stimmen

Moment, wie kann das die Sprachspezifikation komplizieren? Und in dem Moment, in dem die Spezifikation besagt, dass die erste Anweisung ein Konstruktor sein darf, können alle anderen Anweisungen keine Konstruktoren sein. Wenn man die Einschränkung aufhebt, wird die Spezifikation so etwas wie "Sie haben nur Anweisungen innerhalb". Inwiefern ist das komplizierter?

17voto

Randa Sbeity Punkte 370

Ganz einfach, weil dies die Philosophie der Vererbung ist. Und gemäß der Java-Spezifikation ist der Körper des Konstruktors so definiert:

ConstructorBody: { ExplicitConstructorInvocation optieren    BlockStatements optieren }

Die erste Anweisung eines Konstruktorkörpers kann entweder sein

  • ein expliziter Aufruf eines anderen Konstruktors derselben Klasse (unter Verwendung des Schlüsselworts "this"); oder
  • ein expliziter Aufruf der direkten Oberklasse (durch Verwendung des Schlüsselworts "super")

Wenn ein Konstruktorkörper nicht mit einem expliziten Konstruktoraufruf beginnt und der deklarierte Konstruktor nicht Teil der ursprünglichen Klasse Object ist, dann beginnt der Konstruktorkörper implizit mit einem Superklassen-Konstruktoraufruf "super();", einem Aufruf des Konstruktors seiner direkten Superklasse, der keine Argumente annimmt. Und so weiter es gibt eine ganze Kette von Konstruktoren, die bis zum Konstruktor von Object zurückgehen; "Alle Klassen in der Java-Plattform sind Nachkommen von Object". Diese Sache wird " Verkettung von Konstruktoren ".

Warum ist das so?
Und der Grund, warum Java den ConstructorBody auf diese Weise definiert hat, ist, dass sie es brauchten, um die Hierarchie aufrechtzuerhalten des Objekts. Erinnern Sie sich an die Definition der Vererbung: Es ist die Erweiterung einer Klasse. Das heißt, man kann nicht etwas erweitern, das es nicht gibt. Die Basis (die Oberklasse) muss zuerst erstellt werden, dann kann man sie ableiten (die Unterklasse). Deshalb nennt man sie auch Eltern- und Kind-Klassen; man kann kein Kind ohne Elternteil haben.

Technisch gesehen erbt eine Unterklasse alle Mitglieder (Felder, Methoden, verschachtelte Klassen) von ihrer Mutterklasse. Und da Konstruktoren KEINE Mitglieder sind (sie gehören nicht zu Objekten, sondern sind für die Erstellung von Objekten zuständig), werden sie NICHT an Unterklassen vererbt, können aber aufgerufen werden. Und da bei der Erstellung eines Objekts wird nur EIN Konstruktor ausgeführt . Wie können wir also die Erstellung der Oberklasse garantieren, wenn Sie das Objekt der Unterklasse erstellen? Daher das Konzept der "Konstruktorverkettung"; wir haben also die Möglichkeit, andere Konstruktoren (d. h. Superklassen) aus dem aktuellen Konstruktor heraus aufzurufen. Und Java verlangt, dass dieser Aufruf die ERSTE Zeile im Unterklassenkonstruktor sein muss, um die Hierarchie aufrechtzuerhalten und zu garantieren. Sie gehen davon aus, dass, wenn Sie das übergeordnete Objekt nicht explizit ZUERST erstellen (z. B. wenn Sie es vergessen haben), sie es implizit für Sie tun werden.

Diese Prüfung wird während der Kompilierung durchgeführt. Aber ich bin mir nicht sicher, was zur Laufzeit passieren würde, welche Art von Laufzeitfehler wir bekommen würden, WENN Java keinen Kompilierfehler auslöst, wenn wir explizit versuchen, einen Basiskonstruktor aus dem Konstruktor einer Unterklasse in der Mitte seines Körpers auszuführen und nicht aus der allerersten Zeile ...

2 Stimmen

Ich weiß, dass Konstruktoren nicht als Funktionsaufrufe verarbeitet werden, aber ich würde denken, dass die Interpretation jedes Super-Konstruktor-Aufrufs als this = [new object] und die Forderung, dass this definiert werden, bevor es als verwendet wird und bevor ein Konstruktor zurückkehrt, wäre semantisch ausreichend, um die genannten Ziele zu erreichen. Die Unmöglichkeit, die übergeordneten Konstruktoraufrufe in eine try-catch-rethrow o try/finally Block macht es unmöglich, einen Unterklassenkonstruktor versprechen zu lassen, nicht etwas zu werfen, was der Oberklassenkonstruktor auslösen könnte, selbst wenn die Unterklasse in der Lage wäre, zu garantieren...

2 Stimmen

...dass die Ausnahme nicht eintreten kann. Es erhöht auch die Schwierigkeit der sicheren Verkettung von Konstruktoren, die Ressourcen erwerben und an den übergeordneten Konstruktor übergeben müssen (der untergeordnete Konstruktor muss von einer Factory-Methode aufgerufen werden, die einen Container für die Ressourcen erstellt und den Konstruktor innerhalb einer try Block und verwirft alle Ressourcen im Container, wenn der Konstruktor fehlschlägt.

2 Stimmen

Technisch gesehen ist es nicht das erste Zeile sondern vielmehr die erste ausführbare Anweisung im Konstruktor. Es ist völlig legal, Kommentare vor den expliziten Konstruktoraufrufen zu haben.

13voto

Jason S Punkte 178087

Ich bin mir ziemlich sicher (diejenigen, die mit der Java-Spezifikation vertraut sind, mögen sich dazu äußern), dass dies verhindern soll, dass Sie (a) ein teilweise konstruiertes Objekt verwenden dürfen und (b) den Konstruktor der übergeordneten Klasse zwingen, ein "frisches" Objekt zu konstruieren.

Einige Beispiele für eine "schlechte" Sache wären:

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 Stimmen

Sollte Bad1 y Bad2 erweitern. Thing dort?

10 Stimmen

Ich stimme nicht zu mit Bad2 als x wird erklärt in Thing und darf einfach nirgendwo anders gesetzt werden. Wie für Bad1 Sie haben sicher recht, aber etwas Ähnliches kann passieren, wenn der Superkonstruktor eine in der Unterklasse überschriebene Methode aufruft, die auf eine (noch nicht initialisierte) Variable der Unterklasse zugreift. Die Einschränkung hilft also, einen Teil des Problems zu verhindern... was sich IMHO nicht lohnt.

0 Stimmen

@maaartinus der Unterschied ist, dass der Autor des Konstruktors der Oberklasse die Verantwortung für den Aufruf überschreibbarer Methoden hat. Es ist also möglich, die Oberklasse so zu gestalten, dass sie immer einen konsistenten Zustand hat, was nicht möglich wäre, wenn Unterklassen das Objekt verwenden dürften, bevor der Konstruktor der Oberklasse aufgerufen wurde.

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