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

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

public abstract class BasicPage extends WebPage {


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


protected abstract String getTitle();


}

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

139726 次浏览

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

看看这个例子链接http://www.javapractices.com/topic/TopicAction.do?Id=215

在构造函数中调用可重写方法允许子类破坏代码,因此不能保证它还能正常工作。这就是为什么你会得到警告。

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

为了“修复”这个问题,你可以使用工厂方法而不是构造函数,这是对象实例化的常见模式。

关于从构造函数调用可重写方法

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

引用有效的Java第二版,项目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项:当面对许多构造函数参数时,考虑构建器模式的一段话:

传统上,程序员使用可伸缩的构造函数模式,在这种模式中,你提供一个构造函数只包含必需的参数,另一个构造函数包含一个可选参数,第三个构造函数包含两个可选参数,等等……

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

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参数,这真的会把事情搞得一团糟。

正如您所看到的,这不是一个令人愉快的模式,使用起来更不愉快(“true”在这里是什么意思?13是什么?)。

Bloch建议使用一个构建器模式,它允许你写这样的东西:

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

请注意,现在已对参数进行了命名,您可以按照您想要的任何顺序设置它们,并且可以跳过希望保持默认值的参数。这当然比伸缩构造函数好得多,特别是当有大量属于许多相同类型的参数时。

另请参阅

相关问题

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

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()期望什么,你会得到一个警告。

下面是一个例子,它揭示了在超级构造函数中调用可覆盖方法时可能发生的逻辑问题

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内部的可重写方法。但在方法内部,我们使用的实例变量因素具有尚未初始化(因为A的构造函数尚未完成),因此factor为0而不是1,也肯定不是2(程序员可能认为它会是2)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将是多么困难。

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

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

public abstract class BasicPage extends WebPage {


public BasicPage() {
}


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


protected abstract String getTitle();
}

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

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

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

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

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

使它们私人消除所有疑问:"你不能通过"

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

真正的问题是不仅仅是访问修饰符,因为我试图解释在这里。完全诚实地说,private是一个明显的终结者,而protected通常仍然会允许一个(有害的)变通方法。

一个更普遍的建议是:

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

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

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

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

简单地说,你想要单独的准备"从“点火"”

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

PS:考虑实现Closeable接口。