744 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.

64 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.

3voto

Vouze Punkte 1598

Ich habe eine Abhilfe gefunden.

Das lässt sich nicht kompilieren:

public class MySubClass extends MyClass {
    public MySubClass(int a, int b) {
        int c = a + b;
        super(c);  // COMPILE ERROR
        doSomething(c);
        doSomething2(a);
        doSomething3(b);
    }
}

Das funktioniert:

public class MySubClass extends MyClass {
    public MySubClass(int a, int b) {
        this(a + b);
        doSomething2(a);
        doSomething3(b);
    }

    private MySubClass(int c) {
        super(c);
        doSomething(c);
    }
}

4 Stimmen

Die Frage bezieht sich nicht auf eine Umgehungslösung. Tatsächlich findet sich eine Abhilfe in der Frage selbst.

0 Stimmen

Dies ist keine Umgehungslösung. Sie können immer noch nicht mehrere Codezeilen schreiben.

3voto

Jaydev Punkte 1673

Es ist sinnvoll, dass die Konstruktoren ihre Ausführung in der Reihenfolge von Ableitung. Da eine Oberklasse keine Kenntnis von einer Unterklasse hat, ist jede Initialisierung, die sie durchführen muss, getrennt von und möglicherweise Voraussetzung für jede Initialisierung, die von der Unterklasse durchgeführt wird. Daher muss sie ihre Ausführung zuerst abschließen.

Eine einfache Demonstration:

class A {
    A() {
        System.out.println("Inside A's constructor.");
    }
}

class B extends A {
    B() {
        System.out.println("Inside B's constructor.");
    }
}

class C extends B {
    C() {
        System.out.println("Inside C's constructor.");
    }
}

class CallingCons {
    public static void main(String args[]) {
        C c = new C();
    }
}

Die Ausgabe dieses Programms lautet:

Inside A's constructor
Inside B's constructor
Inside C's constructor

0 Stimmen

In diesem Beispiel gibt es in jeder Klasse einen Standardkonstruktor, so dass die Methode super(...,...) in der Unterklasse nicht unbedingt aufgerufen werden muss

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.

2voto

Savvas Dalkitsis Punkte 11241

Sie können anonyme Initialisierungsblöcke verwenden, um Felder im Kind zu initialisieren, bevor Sie dessen Konstruktor aufrufen. Dieses Beispiel zeigt:

public class Test {
    public static void main(String[] args) {
        new Child();
    }
}

class Parent {
    public Parent() {
        System.out.println("In parent");
    }
}

class Child extends Parent {

    {
        System.out.println("In initializer");
    }

    public Child() {
        super();
        System.out.println("In child");
    }
}

Dies führt zur Ausgabe von :

Bei Eltern
Im Initialisierer
Im Kind

3 Stimmen

Dies bringt jedoch keinen zusätzlichen Nutzen gegenüber dem Hinzufügen der System.out.println("In initializer") als erste Zeile nach "super()", oder? Was nützlich wäre, wäre eine Möglichkeit, Code auszuführen vor die Elternteil aufgebaut ist.

0 Stimmen

In der Tat. Wenn Sie versuchen, etwas hinzuzufügen, müssen Sie den berechneten Zustand irgendwo speichern. Selbst wenn der Compiler dies zulässt, wie wird der temporäre Speicher aussehen? Ein weiteres Feld nur für die Initialisierung zuweisen? Aber das ist Speicherverschwendung.

2 Stimmen

Dies ist falsch. Instanzinitialisierungen werden nach der Rückkehr des übergeordneten Konstruktoraufrufs eingefügt.

1voto

mid Punkte 410

Ich weiß, dass ich ein bisschen spät dran bin, aber ich habe diesen Trick schon ein paar Mal angewandt (und ich weiß, dass er etwas ungewöhnlich ist):

Ich erstelle eine generische Schnittstelle InfoRunnable<T> mit einer Methode:

public T run(Object... args);

Und wenn ich etwas tun muss, bevor ich es an den Konstruktor übergebe, mache ich einfach dies:

super(new InfoRunnable<ThingToPass>() {
    public ThingToPass run(Object... args) {
        /* do your things here */
    }
}.run(/* args here */));

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