it-swarm.com.de

Was ist falsch an überschreibbaren Methodenaufrufen in Konstruktoren?

Ich habe eine Wicket-Seitenklasse, die den Seitentitel abhängig vom Ergebnis einer abstrakten Methode festlegt.

public abstract class BasicPage extends WebPage {

    public BasicPage() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();

}

NetBeans warnt mich mit der Meldung "Überschreibbarer Methodenaufruf im Konstruktor", aber was sollte daran falsch sein? Die einzige Alternative, die ich mir vorstellen kann, besteht darin, die Ergebnisse ansonsten abstrakter Methoden in Unterklassen an den Superkonstruktor zu übergeben. Bei vielen Parametern könnte dies jedoch schwer zu lesen sein.

363
deamon

Beim Aufrufen einer überschreibbaren Methode von Konstruktoren

Einfach ausgedrückt, das ist falsch, weil es unnötig Möglichkeiten für [~ # ~] viele [~ # ~] Fehler eröffnet. Wenn @Override Aufgerufen wird, ist der Status des Objekts möglicherweise inkonsistent und/oder unvollständig.

Ein Zitat aus Effective Java 2nd Edition, Punkt 17: Design und Dokument zur Vererbung, oder verbieten Sie es:

Es gibt einige weitere Einschränkungen, die eine Klasse beachten muss, um die Vererbung zuzulassen. Konstruktoren dürfen weder direkt noch indirekt überschreibbare Methoden aufrufen. Wenn Sie gegen diese Regel verstoßen, führt dies zu einem Programmfehler. Der Superklassenkonstruktor wird vor dem Unterklassenkonstruktor ausgeführt, sodass die überschreibende Methode in der Unterklasse aufgerufen wird, bevor der Unterklassenkonstruktor ausgeführt wurde. Wenn die überschreibende Methode von einer Initialisierung abhängt, die vom Unterklassenkonstruktor ausgeführt wird, verhält sich die Methode nicht wie erwartet.

Hier ist ein Beispiel zur Veranschaulichung:

public class ConstructorCallsOverride {
    public static void main(String[] args) {

        abstract class Base {
            Base() {
                overrideMe();
            }
            abstract void overrideMe(); 
        }

        class Child extends Base {

            final int x;

            Child(int x) {
                this.x = x;
            }

            @Override
            void overrideMe() {
                System.out.println(x);
            }
        }
        new Child(42); // prints "0"
    }
}

Wenn der Konstruktor BaseoverrideMe aufruft, hat Child die Initialisierung von final int x Noch nicht abgeschlossen, und die Methode erhält den falschen Wert. Dies wird mit ziemlicher Sicherheit zu Fehlern und Irrtümern führen.

Verwandte Fragen

Siehe auch


Über Objektkonstruktion mit vielen Parametern

Konstruktoren mit vielen Parametern können zu schlechter Lesbarkeit führen, und es gibt bessere Alternativen.

Hier ist ein Zitat aus Effective Java 2nd Edition, Punkt 2: Betrachten Sie ein Builder-Muster, wenn Sie mit vielen Konstruktorparametern konfrontiert werden:

Herkömmlicherweise haben Programmierer das Muster Teleskopkonstruktor verwendet, bei dem Sie einem Konstruktor nur die erforderlichen Parameter, einem anderen einen einzelnen optionalen Parameter, einem dritten zwei optionale Parameter usw. zuweisen. .

Das Konstruktormuster für Teleskope sieht im Wesentlichen so aus:

public class Telescope {
    final String name;
    final int levels;
    final boolean isAdjustable;

    public Telescope(String name) {
        this(name, 5);
    }
    public Telescope(String name, int levels) {
        this(name, levels, false);
    }
    public Telescope(String name, int levels, boolean isAdjustable) {       
        this.name = name;
        this.levels = levels;
        this.isAdjustable = isAdjustable;
    }
}

Und jetzt können Sie Folgendes tun:

new Telescope("X/1999");
new Telescope("X/1999", 13);
new Telescope("X/1999", 13, true);

Sie können derzeit jedoch nicht nur name und isAdjustable festlegen und die Standardeinstellung levels beibehalten. Sie können mehr Konstruktorüberladungen bereitstellen, aber offensichtlich würde die Anzahl mit zunehmender Anzahl von Parametern explodieren, und Sie könnten sogar mehrere Argumente boolean und int haben, die die Dinge wirklich durcheinander bringen würden .

Wie Sie sehen, ist dieses Muster nicht angenehm zu schreiben und noch weniger angenehm zu verwenden (Was bedeutet "wahr" hier? Was ist 13?).

Bloch empfiehlt die Verwendung eines Builder-Musters, mit dem Sie stattdessen Folgendes schreiben können:

Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();

Beachten Sie, dass die Parameter jetzt benannt sind und Sie sie in einer beliebigen Reihenfolge festlegen können. Außerdem können Sie diejenigen überspringen, deren Standardwerte beibehalten werden sollen. Dies ist sicherlich viel besser als teleskopierende Konstruktoren, insbesondere wenn es eine große Anzahl von Parametern gibt, die zu vielen der gleichen Typen gehören.

Siehe auch

Verwandte Fragen

458

Hier ist ein Beispiel, das hilft, dies zu verstehen:

public class Main {
    static abstract class A {
        abstract void foo();
        A() {
            System.out.println("Constructing A");
            foo();
        }
    }

    static class C extends A {
        C() { 
            System.out.println("Constructing C");
        }
        void foo() { 
            System.out.println("Using C"); 
        }
    }

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

Wenn Sie diesen Code ausführen, erhalten Sie die folgende Ausgabe:

Constructing A
Using C
Constructing C

Siehst du? foo() verwendet C, bevor der Konstruktor von C ausgeführt wurde. Wenn foo() erfordert, dass C einen definierten Zustand hat (d. H. Der Konstruktor ist beendet), dann wird es einen undefinierten Zustand in C finden und Dinge könnten brechen. Und da Sie in A nicht wissen können, was das überschriebene foo() erwartet, erhalten Sie eine Warnung.

56

Durch das Aufrufen einer überschreibbaren Methode im Konstruktor können Unterklassen den Code unterwandern, sodass Sie nicht garantieren können, dass er nicht mehr funktioniert. Deshalb bekommst du eine Warnung.

Was passiert in Ihrem Beispiel, wenn eine Unterklasse getTitle() überschreibt und null zurückgibt?

Um dies zu "beheben", können Sie eine Factory-Methode anstelle eines Konstruktors verwenden. Dies ist ein übliches Muster für die Instanziierung von Objekten.

11
KeatsPeeks

Wenn Sie in Ihrem Konstruktor Methoden aufrufen, die Unterklassen überschreiben, ist es weniger wahrscheinlich, dass Sie auf noch nicht vorhandene Variablen verweisen, wenn Sie Ihre Initialisierung logisch zwischen dem Konstruktor und der Methode aufteilen.

Schauen Sie sich diesen Beispiellink an http://www.javapractices.com/topic/TopicAction.do?Id=215

4
Manuel Selva

Das folgende Beispiel zeigt die logischen Probleme, die beim Aufrufen einer überschreibbaren Methode im Superkonstruktor auftreten können.

class A {

    protected int minWeeklySalary;
    protected int maxWeeklySalary;

    protected static final int MIN = 1000;
    protected static final int MAX = 2000;

    public A() {
        setSalaryRange();
    }

    protected void setSalaryRange() {
        throw new RuntimeException("not implemented");
    }

    public void pr() {
        System.out.println("minWeeklySalary: " + minWeeklySalary);
        System.out.println("maxWeeklySalary: " + maxWeeklySalary);
    }
}

class B extends A {

    private int factor = 1;

    public B(int _factor) {
        this.factor = _factor;
    }

    @Override
    protected void setSalaryRange() {
        this.minWeeklySalary = MIN * this.factor;
        this.maxWeeklySalary = MAX * this.factor;
    }
}

public static void main(String[] args) {
    B b = new B(2);
    b.pr();
}

Das Ergebnis wäre tatsächlich:

minWocheGehalt: 0

maxWeeklySalary: 0

Dies liegt daran, dass der Konstruktor der Klasse B zuerst den Konstruktor der Klasse A aufruft, in dem die überschreibbare Methode in B ausgeführt wird. Aber innerhalb der Methode verwenden wir die Instanzvariable Faktor, die noch nicht initialisiert ist (weil der Konstruktor von A noch nicht fertig ist), also ist der Faktor 0 und nicht 1 und definitiv nicht 2 (die Sache, von der der Programmierer denken könnte, dass sie es sein wird). Stellen Sie sich vor, wie schwer es wäre, einen Fehler aufzuspüren, wenn die Berechnungslogik zehnmal so verdreht wäre.

Ich hoffe das würde jemandem helfen.

4
kirilv

Im speziellen Fall von Wicket: Dies ist genau der Grund, warum ich die Wicket-Entwickler gebeten habe, Unterstützung für einen expliziten Zweiphasenkomponenten-Initialisierungsprozess im Lebenszyklus des Frameworks zum Erstellen einer Komponente hinzuzufügen, d. H.

  1. Konstruktion - über Konstruktor
  2. Initialisierung - über onInitilize (nach der Erstellung, wenn virtuelle Methoden funktionieren!)

Es gab eine ziemlich aktive Debatte darüber, ob es notwendig war oder nicht (es ist IMHO völlig notwendig), wie dieser Link zeigt http://Apache-wicket.1842946.n4.nabble.com/VOTE -WICKET-3218-Component-onInitialize-is-broken-for-Pages-td3341090i20.html )

Die gute Nachricht ist, dass die hervorragenden Entwickler von Wicket am Ende eine zweiphasige Initialisierung eingeführt haben (um das schrecklichste Java UI-Framework noch fantastischer!)), Sodass Sie mit Wicket alle Ihre Post-Konstruktionen durchführen können Initialisierung in der onInitialize-Methode, die vom Framework automatisch aufgerufen wird, wenn Sie es überschreiben. Zu diesem Zeitpunkt im Lebenszyklus Ihrer Komponente hat der Konstruktor seine Arbeit abgeschlossen, sodass virtuelle Methoden wie erwartet funktionieren.

2
Volksman

Ich denke, für Wicket ist es besser, die add -Methode in der onInitialize() aufzurufen (siehe Komponentenlebenszyklus ):

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

    @Override
    public void onInitialize() {
        add(new Label("title", getTitle()));
    }

    protected abstract String getTitle();
}
0
sanluck