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

public abstract class BasicPage extends WebPage {

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

    protected abstract String getTitle();

}

NetBeans用“构造函数中可重写方法调用”的消息警告我,但是它应该有什么问题呢?我能想到的唯一替代方法是将抽象方法的结果传递给子类中的超构造函数。但考虑到很多参数,这可能很难解读。


当前回答

下面的示例揭示了在超级构造函数中调用可重写方法时可能出现的逻辑问题。

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

最大每周工资: 0

这是因为类B的构造函数首先调用类A的构造函数,在那里执行B内部的可重写方法。但是在方法内部,我们使用的实例变量factor还没有初始化(因为A的构造函数还没有完成),因此factor是0而不是1,也肯定不是2(程序员可能认为它会是2)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将是多么困难。

我希望这能帮助到一些人。

其他回答

我当然同意,有些情况下最好不要从构造函数调用某些方法。

让它们成为私人空间可以消除所有的疑虑:“你不能通过”。

然而,如果你真的想保持开放呢?

正如我在这里试图解释的那样,真正的问题不仅仅是访问修饰符。坦率地说,隐私是一个明显的阻碍,受保护通常仍然会允许一个(有害的)变通办法。

一个更普遍的建议是:

不要从构造函数启动线程 不要从构造函数中读取文件 不要从构造函数中调用api或服务 不要从构造函数中加载数据库中的数据 不要从构造函数中解析json或XML文档

不要直接从构造函数中这样做。这包括从构造函数调用的private/protected函数执行这些操作。

从构造函数调用start()方法肯定是一个危险信号。

相反,你应该提供一个公共的init(), start()或connect()方法。把责任留给消费者。

简单地说,你要把“准备”和“点火”分开。

如果构造函数可以扩展,那么它就不应该自燃。 如果它自燃,那么它就有可能在完全建造之前发射。 毕竟,将来可以在子类的构造函数中添加更多的准备。而且你不能控制超类构造函数的执行顺序。

PS:考虑同时实现Closeable接口。

在Wicket的具体情况下:这就是我问Wicket的原因 开发人员在构建组件的框架生命周期中增加了对显式的两阶段组件初始化过程的支持。

构造——通过构造函数 初始化-通过oninitialize(在构造虚拟方法工作后!)

关于是否有必要(恕我直言,这是完全必要的),有相当活跃的辩论,因为这个链接展示了http://apache-wicket.1842946.n4.nabble.com/VOTE-WICKET-3218-Component-onInitialize-is-broken-for-Pages-td3341090i20.html)

好消息是,Wicket的优秀开发人员最终引入了两阶段初始化(使最出色的Java UI框架更加出色!),所以在Wicket中,你可以在onInitialize方法中完成所有的构造后初始化,如果你重写它,框架会自动调用它——在组件生命周期的这一点上,它的构造函数已经完成了它的工作,因此虚拟方法可以正常工作。

我想对于Wicket来说,最好在onInitialize()中调用add方法(参见组件生命周期):

public abstract class BasicPage extends WebPage {

    public BasicPage() {
    }

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

    protected abstract String getTitle();
}

下面的示例揭示了在超级构造函数中调用可重写方法时可能出现的逻辑问题。

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

最大每周工资: 0

这是因为类B的构造函数首先调用类A的构造函数,在那里执行B内部的可重写方法。但是在方法内部,我们使用的实例变量factor还没有初始化(因为A的构造函数还没有完成),因此factor是0而不是1,也肯定不是2(程序员可能认为它会是2)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将是多么困难。

我希望这能帮助到一些人。

下面的例子有助于理解这一点:

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

你看到了什么?foo()在运行C的构造函数之前使用C。如果foo()要求C有一个定义的状态(即构造函数已经完成),那么它将在C中遇到一个未定义的状态,事情可能会破裂。由于您无法知道在A中重写的foo()期望什么,因此您将得到一个警告。