构造函数中的可覆盖方法调用有什么问题?

时间:2010-08-04 09:41:44

标签: java oop inheritance constructor override

我有一个Wicket页面类,它根据抽象方法的结果设置页面标题。

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

NetBeans通过消息“构造函数中的可覆盖方法调用”警告我,但它应该有什么问题?我能想象的唯一选择是将其他抽象方法的结果传递给子类中的超级构造函数。但是很多参数都难以阅读。

7 个答案:

答案 0 :(得分:454)

从构造函数

调用可覆盖的方法

简单地说,这是错误的,因为它不必要地为 MANY 错误开辟了可能性。当调用@Override时,对象的状态可能不一致和/或不完整。

引用来自 Effective Java 2nd Edition,第17项:继承的设计和文档,或者禁止它

  

为了允许继承,类必须遵守一些限制。 构造函数不得直接或间接调用可覆盖的方法。如果违反此规则,将导致程序失败。超类构造函数在子类构造函数之前运行,因此在子类构造函数运行之前将调用子类中的重写方法。如果重写方法依赖于子类构造函数执行的任何初始化,则该方法将不会按预期运行。

以下是一个例子来说明:

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

此处,当Base构造函数调用overrideMe时,Child尚未完成初始化final int x,并且该方法的值不正确。这几乎肯定会导致错误和错误。

相关问题

另见


关于具有许多参数的对象构造

具有许多参数的构造函数可能导致可读性差,并且存在更好的替代方案。

以下是 Effective Java 2nd Edition的引用,第2项:在面对许多构造函数参数时考虑构建器模式

  

传统上,程序员使用 telescoping构造函数模式,在该模式中,您只提供构造函数所需的参数,另一个使用单个可选参数,第三个使用两个可选参数,依此类推。 ..

伸缩构造函数模式基本上是这样的:

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

现在您可以执行以下任何操作:

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

但是,您目前无法仅设置nameisAdjustable,并且默认保留levels。你可以提供更多的构造函数重载,但显然随着参数数量的增加,数字会爆炸,你甚至可能有多个booleanint参数,这实际上会让事情搞得一团糟。 / p>

正如你所看到的,这不是一个令人愉快的写作模式,使用起来也不那么令人愉快(“真实”在这里意味着什么?13是什么?)。

Bloch建议使用一个构建器模式,这样你就可以编写类似这样的内容:

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

请注意,现在参数已命名,您可以按任何顺序设置它们,并且可以跳过要保留默认值的参数。这肯定比伸缩构造函数好得多,特别是当存在大量属于许多相同类型的参数时。

另见

相关问题

答案 1 :(得分:54)

这是一个有助于理解这个问题的例子:

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

如果运行此代码,则会得到以下输出:

Constructing A
Using C
Constructing C
你知道吗? {C}构造函数运行之前foo()使用了C语言。如果foo()要求C具有已定义的状态(即构造函数已完成),那么它将在C中遇到未定义的状态,并且事情可能会中断。而且由于你无法在A中知道被覆盖的foo()所期望的内容,因此会收到警告。

答案 2 :(得分:11)

在构造函数中调用可覆盖的方法允许子类破坏代码,因此您无法保证它可以再次运行。这就是你收到警告的原因。

在您的示例中,如果子类重写getTitle()并返回null,会发生什么?

要“修复”这个,您可以使用factory method而不是构造函数,这是对象实例的常见模式。

答案 3 :(得分:4)

如果在构造函数中调用子类覆盖的方法,则意味着如果在构造函数和方法之间逻辑划分初始化,则不太可能引用不存在的变量。

查看此示例链接http://www.javapractices.com/topic/TopicAction.do?Id=215

答案 4 :(得分:4)

这是一个示例,揭示了在超级构造函数中调用可覆盖方法时可能出现的逻辑问题

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

结果实际上是:

minWeeklySalary:0

maxWeeklySalary:0

这是因为B类的构造函数首先调用类A的构造函数,其中B中的可覆盖方法被执行。但是在方法中我们使用的实例变量 factor 已经尚未初始化(因为A的构造函数尚未完成),因此因子是0而不是1绝对不是2(程序员可能会想到的东西)。想象一下,如果计算逻辑的扭曲度是十倍,那么跟踪错误会有多难。

我希望这会对某人有所帮助。

答案 5 :(得分:2)

在Wicket的具体案例中:这就是我问Wicket的原因 开发人员在框架构建组件的生命周期中添加对显式两阶段组件初始化过程的支持,即

  1. 构造 - 通过构造函数
  2. 初始化 - 通过onInitilize(虚拟方法工作后构建!)
  3. 关于是否有必要进行了充分的辩论(完全有必要恕我直言),因为此链接显示http://apache-wicket.1842946.n4.nabble.com/VOTE-WICKET-3218-Component-onInitialize-is-broken-for-Pages-td3341090i20.html

    好消息是,Wicket的优秀开发人员确实最终引入了两个阶段的初始化(以使最令人难以置信的Java UI框架更加出色!)所以使用Wicket,你可以在onInitialize方法中完成所有的post构造初始化。如果你覆盖它,框架会自动调用它 - 在组件生命周期的这一点上,它的构造函数已经完成了它的工作,所以虚拟方法按预期工作。

答案 6 :(得分:0)

我想Wicket最好在onInitialize()中调用public abstract class BasicPage extends WebPage { public BasicPage() { } @Override public void onInitialize() { add(new Label("title", getTitle())); } protected abstract String getTitle(); } 方法(参见components lifecycle):

    With Sheets("Line_Site")
        .Cells(x, "E").NumberFormat = "@"
        .Cells(x, "E") = .Cells(x, "A").Text & .Cells(x, "B").Text & .Cells(x, "C").Text
    End With