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.

9voto

Kate Gregory Punkte 18638

Du hast gefragt, warum, und die anderen Antworten sagen imo nicht wirklich, warum es in Ordnung ist, den Konstruktor deines Super aufzurufen, aber nur, wenn es die allererste Zeile ist. Der Grund ist, dass du nicht wirklich aufrufen den Konstrukteur. In C++ lautet die entsprechende Syntax

MySubClass: MyClass {

public:

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

};

Wenn Sie die Initialisierungsklausel allein vor der öffnenden Klammer sehen, wissen Sie, dass sie etwas Besonderes ist. Sie wird ausgeführt, bevor der Rest des Konstruktors ausgeführt wird und sogar bevor die Mitgliedsvariablen initialisiert werden. Bei Java ist es nicht viel anders. Es gibt eine Möglichkeit, einen Teil des Codes (andere Konstruktoren) auszuführen, bevor der Konstruktor wirklich beginnt, bevor irgendwelche Mitglieder der Unterklasse initialisiert werden. Und dieser Weg ist, den "Aufruf" (z.B. super ) gleich in der ersten Zeile. (In gewisser Weise ist das super o this ist sozusagen vor der ersten geschweiften Klammer, auch wenn Sie ihn danach eingeben, weil er ausgeführt wird, bevor Sie zu dem Punkt kommen, an dem alles vollständig aufgebaut ist). Jeder andere Code nach der öffnenden Klammer (wie int c = a + b; ) lässt den Compiler sagen: "Oh, ok, keine anderen Konstruktoren, dann können wir alles initialisieren." Also läuft er los und initialisiert Ihre Superklasse und Ihre Mitglieder und so weiter und beginnt dann mit der Ausführung des Codes nach der offenen Klammer.

Wenn ein paar Zeilen später ein Code auftaucht, der sagt: "Oh ja, wenn du dieses Objekt konstruierst, hier sind die Parameter, die du an den Konstruktor der Basisklasse weitergeben sollst", ist es zu spät und es macht keinen Sinn mehr. Sie erhalten also einen Compilerfehler.

2 Stimmen

1. Wenn die Java-Entwickler wollten, dass der Superkonstruktor implizit ist, könnten sie das einfach tun, und, was noch wichtiger ist, dies erklärt nicht, warum ein impliziter Superkonstruktor sehr nützlich ist. 2. IMO ist Ihr Kommentar, dass es keinen Sinn macht, nicht sinnvoll. Ich erinnere mich, dass ich das gebraucht habe. Können Sie beweisen, dass ich etwas Sinnloses getan habe?

2 Stimmen

Stellen Sie sich vor, Sie müssen einen Raum betreten. Die Tür ist verschlossen, also schlagen Sie ein Fenster ein, greifen hinein und verschaffen sich Zutritt. Drinnen, auf halbem Weg durch den Raum, finden Sie einen Zettel mit einem Schlüssel, den Sie auf dem Weg nach drinnen benutzen sollen. Aber Sie sind ja schon drin. Ähnlich verhält es sich, wenn der Compiler auf halbem Weg durch die Ausführung eines Konstruktors auf den Hinweis stößt: "Hier steht, was mit diesen Parametern zu tun ist, bevor der Konstruktor ausgeführt wird", was soll er tun?

2 Stimmen

Wenn es in Wirklichkeit dumm ist, dann ist es eine falsche Analogie. Wenn ich in der Lage bin zu entscheiden, welchen Weg ich gehen soll, dann bin ich nicht auf halbem Weg. Es ist die Regel, dass der Supercall der erste im Konstruktor sein muss, die uns dazu verleitet, das Fenster einzuschlagen (siehe viele Beispiele des Herumwurschtelns in den Fragen und Antworten), anstatt die Tür zu benutzen. Sie stellen also alles auf den Kopf, wenn Sie versuchen, für diese Regel zu argumentieren. Die Regel muss also falsch sein.

6voto

Tip-Sy Punkte 810

Es hindert Sie also nicht daran, die Logik vor dem Aufruf von super. Es hindert Sie nur daran, Logik auszuführen, die Sie nicht in einen in einen einzigen Ausdruck passt.

Tatsächlich können Sie Logik mit mehreren Abläufen ausführen, Sie müssen Ihren Code nur in eine statische Funktion verpacken und diese in der Superanweisung aufrufen.

Anhand Ihres Beispiels:

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

1 Stimmen

Dies funktioniert nur, wenn der Konstruktor der Superklasse ein einzelnes, nicht-voides Argument erwartet

0 Stimmen

Anstatt sich für die super()/this() als erste Anweisung, hätten die Sprachdesigner festlegen können, dass keine Instanzmethoden oder geerbten Methoden aufgerufen werden können, bevor super()/this() . Vielleicht möchte der Auftraggeber wissen, warum es nicht auf diese Weise gemacht wurde.

5voto

DaveFar Punkte 6589

Ich stimme voll und ganz zu, die Einschränkungen sind zu stark. Es ist nicht immer möglich, eine statische Hilfsmethode zu verwenden (wie Tom Hawtin - tackline vorgeschlagen hat) oder alle "pre-super()-Berechnungen" in einen einzigen Ausdruck im Parameter zu packen, z.B.:

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

Die Verwendung einer Ausnahme "Objekt noch nicht konstruiert", wie von Carson Myers vorgeschlagen, würde helfen, aber die Überprüfung dieser Ausnahme bei jeder Objektkonstruktion würde die Ausführung verlangsamen. Ich würde einen Java-Compiler bevorzugen, der eine bessere Differenzierung vornimmt (anstatt eine if-Anweisung zu verbieten, aber den ?-Operator innerhalb des Parameters zuzulassen), auch wenn dies die Sprachspezifikation verkompliziert.

2 Stimmen

Ich denke, die Ablehnung kommt daher, dass Sie nicht auf die Frage antworten, sondern Kommentare zum Thema abgeben. In einem Forum wäre das in Ordnung, aber SO/SE ist kein Forum :)

0 Stimmen

Hervorragendes Beispiel für die Möglichkeiten der ?: Der Typ von construct kann Sie überraschen. Ich dachte, als ich las: "Es ist nicht unmöglich --- benutze einfach eine ternäre Oper... Oh.".

3voto

Yan Chun Tang Punkte 3

Dies ist eine offizielle Wiederholung: Historisch gesehen, muss this() oder super() in einem Konstruktor an erster Stelle stehen. Dies
Die Beschränkung war nie beliebt und wurde als willkürlich empfunden. Es gab eine
eine Reihe von subtilen Gründen, darunter die Überprüfung von invokespecial,
die zu dieser Einschränkung beigetragen haben. Im Laufe der Jahre haben wir uns mit folgenden Themen befasst
auf VM-Ebene, bis zu dem Punkt, an dem es praktisch wird, diese
in Erwägung ziehen, diese Beschränkung aufzuheben, und zwar nicht nur für Aufzeichnungen, sondern für alle
Konstrukteure.

3voto

John McClane Punkte 3338

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

class Good {
    int essential1;
    int essential2;

    Good(int n) {
        if (n > 100)
            throw new IllegalArgumentException("n is too large!");
        essential1 = 1 / n;
        essential2 = n + 2;
    }
}

class Bad extends Good {
    Bad(int n) {
        try {
            super(n);
        } catch (Exception e) {
            // Exception is ignored
        }
    }

    public static void main(String[] args) {
        Bad b = new Bad(0);
//        b = new Bad(101);
        System.out.println(b.essential1 + b.essential2);
    }
}

Eine Ausnahme während der Konstruktion deutet fast immer darauf hin, dass das zu konstruierende Objekt nicht ordnungsgemäß initialisiert werden konnte, sich nun in einem schlechten Zustand befindet, unbrauchbar ist und entsorgt werden muss. Ein Konstruktor einer Unterklasse hat jedoch die Möglichkeit, eine in einer seiner Oberklassen aufgetretene Ausnahme zu ignorieren und ein teilweise initialisiertes Objekt zurückzugeben. Im obigen Beispiel, wenn das Argument, das an new Bad() entweder 0 oder größer als 100 ist, dann ist weder essential1 noch essential2 ordnungsgemäß initialisiert sind.

Sie mögen sagen, dass es immer eine schlechte Idee ist, Ausnahmen zu ignorieren. OK, hier ist ein weiteres Beispiel:

class Bad extends Good {
    Bad(int n) {
        for (int i = 0; i < n; i++)
            super(i);
    }
}

Komisch, nicht wahr? Wie viele Objekte erstellen wir in diesem Beispiel? Eins? Zwei? Oder vielleicht gar nichts...

Erlaubt den Aufruf super() o this() in der Mitte eines Konstruktors würde die Büchse der Pandora für abscheuliche Konstruktoren öffnen.


Andererseits verstehe ich die häufige Notwendigkeit, einen statischen Teil vor einem Aufruf von super() o this() . Dies kann jeder Code sein, der sich nicht auf this Referenz (die in der Tat bereits am Anfang eines Konstruktors existiert, aber nicht ordnungsgemäß verwendet werden kann, bis super() o this() zurück) und musste einen solchen Anruf tätigen. Außerdem besteht, wie bei jeder Methode, die Möglichkeit, dass einige lokale Variablen, die vor dem Aufruf von super() o this() wird danach benötigt.

In solchen Fällen haben Sie die folgenden Möglichkeiten:

  1. Verwenden Sie das Muster unter diese Antwort die es ermöglicht, die Beschränkung zu umgehen.
  2. Warten Sie auf die Freigabe durch das Java-Team super() und vor this() Code. Dies kann dadurch geschehen, dass eine Beschränkung auferlegt wird, wo super() o this() kann in einem Konstruktor vorkommen. Tatsächlich ist sogar der heutige Compiler in der Lage, gute und schlechte (oder potenziell schlechte) Fälle so gut zu unterscheiden, dass eine statische Codeerweiterung am Anfang eines Konstruktors sicher möglich ist. Nehmen wir an, dass super() y this() return this Referenz und Ihr Konstruktor hat seinerseits

    return this;

am Ende. Außerdem lehnt der Compiler den Code ab

public int get() {
    int x;
    for (int i = 0; i < 10; i++)
        x = i;
    return x;
}

public int get(int y) {
    int x;
    if (y > 0)
        x = y;
    return x;
}

public int get(boolean b) {
    int x;
    try {
        x = 1;
    } catch (Exception e) {
    }
    return x;
}

mit der Fehlermeldung "Variable x wurde möglicherweise nicht initialisiert", könnte es dies bei this Variable und überprüft sie wie jede andere lokale Variable. Der einzige Unterschied ist this kann auf keine andere Weise zugewiesen werden als super() o this() Aufruf (und, wie üblich, wenn es keinen solchen Aufruf bei einem Konstruktor gibt, super() wird vom Compiler implizit am Anfang eingefügt) und darf nicht doppelt zugewiesen werden. Im Zweifelsfall (wie bei der ersten get() , donde x tatsächlich immer zugewiesen wird), könnte der Compiler einen Fehler zurückgeben. Das wäre besser, als bei jedem Konstruktor einen Fehler zurückzugeben, bei dem etwas anderes als ein Kommentar vor super() o this() .

0 Stimmen

Dies ist spät, aber Sie könnten auch das Fabrikmuster verwenden. Machen Sie die Konstruktoren privat. Machen Sie statische Methoden mit den Konstruktoren verbunden. Nennen wir die Klasse Foo, 2 Konstruktoren, Foo() und Foo(int i), und statische Methoden, die sie konstruieren, createFoo() und createFoo(int i). Dann ersetzen Sie this() durch Foo.createFoo(). Sie können also Dinge in createFoo(int i) und zuletzt in Foo.createFoo erledigen. Oder jede andere Reihenfolge. Es ist so etwas wie ein Factory Design Pattern, aber nicht.

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