我有一个 Wicket 页面类,它根据抽象方法的结果设置页面标题。
public abstract class BasicPage extends WebPage {
public BasicPage() {
add(new Label("title", getTitle()));
}
protected abstract String getTitle();
}
NetBeans 用“构造函数中的可覆盖方法调用”消息警告我,但它应该有什么问题?我能想象的唯一选择是将其他抽象方法的结果传递给子类中的超级构造函数。但这可能很难用许多参数来阅读。
从构造函数调用可覆盖的方法
简而言之,这是错误的,因为它不必要地为 许多 错误打开了可能性。调用 @Override
时,对象的状态可能不一致和/或不完整。
引自 Effective Java 2nd Edition,Item 17: Design and document for inheritance,或者禁止它:
一个类必须遵守更多的限制以允许继承。构造函数不得直接或间接调用可覆盖的方法。如果您违反此规则,将导致程序失败。超类构造函数在子类构造函数之前运行,因此子类中的覆盖方法将在子类构造函数运行之前被调用。如果覆盖方法依赖于子类构造函数执行的任何初始化,则该方法将不会按预期运行。
这里有一个例子来说明:
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
的初始化,并且该方法得到了错误的值。这几乎肯定会导致错误和错误。
相关问题
从父类构造函数调用重写的方法
Java中基类构造函数调用重写方法时派生类对象的状态
在抽象类的构造函数中使用抽象 init() 函数
也可以看看
FindBugs - 从超类的构造函数调用的字段方法的未初始化读取
多参数对象构造
具有许多参数的构造函数会导致可读性差,但存在更好的替代方案。
这是来自 Effective Java 2nd Edition,Item 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);
但是,您目前不能只设置 name
和 isAdjustable
,而将 levels
保留为默认值。您可以提供更多的构造函数重载,但显然数量会随着参数数量的增加而爆炸式增长,您甚至可能有多个 boolean
和 int
参数,这确实会使事情变得一团糟。
如您所见,这不是一个令人愉快的编写模式,使用起来更不愉快(这里的“true”是什么意思?13 是多少?)。
Bloch 建议使用构建器模式,它允许您编写类似这样的东西:
Telescope telly = new Telescope.Builder("X/1999").setAdjustable(true).build();
请注意,现在参数已命名,您可以按所需的任何顺序设置它们,并且可以跳过要保留默认值的参数。这肯定比伸缩构造函数好得多,尤其是当有大量参数属于许多相同类型时。
也可以看看
维基百科/生成器模式
Effective Java 第 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()
期望什么,因此您会收到警告。
在构造函数中调用可覆盖的方法允许子类颠覆代码,因此您不能保证它不再起作用。这就是你收到警告的原因。
在您的示例中,如果子类覆盖 getTitle()
并返回 null 会发生什么?
要“修复”这个问题,您可以使用 factory method 而不是构造函数,这是对象实例化的常见模式。
null
是破坏许多接口的一般问题。
这是一个示例,它揭示了在超级构造函数中调用可覆盖方法时可能出现的逻辑问题。
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 的构造函数尚未完成),因此 factor 是 0 而不是 1 也绝对不是 2(程序员可能认为它会是)。想象一下,如果计算逻辑扭曲十倍,跟踪错误将是多么困难。
我希望这会对某人有所帮助。
如果您在构造函数中调用子类覆盖的方法,这意味着如果您在构造函数和方法之间逻辑划分初始化,则不太可能引用尚不存在的变量。
查看此示例链接 http://www.javapractices.com/topic/TopicAction.do?Id=215
在 Wicket 的特定情况下:这就是为什么我要求 Wicket 开发人员在构建组件的框架生命周期中添加对显式两阶段组件初始化过程的支持的原因,即
构造 - 通过构造函数初始化 - 通过 onInitilize (在虚拟方法工作时构造之后!)
关于是否有必要(恕我直言,这完全是必要的)存在相当激烈的辩论,因为此链接显示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
方法(参见 components lifecycle):
public abstract class BasicPage extends WebPage {
public BasicPage() {
}
@Override
public void onInitialize() {
add(new Label("title", getTitle()));
}
protected abstract String getTitle();
}
我当然同意在某些情况下最好不要从构造函数中调用某些方法。
将它们设为私有可以消除所有疑问:"You shall not pass"。
但是,如果您确实想保持开放状态怎么办。
不只是访问修饰符才是真正的问题,正如我试图解释的 here。老实说,private
是一个明显的阻碍,其中 protected
通常仍然允许(有害的)解决方法。
更一般的建议:
不要从你的构造函数启动线程
不要从构造函数中读取文件
不要从构造函数调用 API 或服务
不要从构造函数的数据库中加载数据
不要从构造函数中解析 json 或 xml 文档
不要(in)直接从您的构造函数中这样做。这包括从构造函数调用的私有/受保护函数执行任何这些操作。
从您的构造函数调用 start()
方法肯定是一个危险信号。
相反,您应该提供 public init()
、start()
或 connect()
方法。并将责任留给消费者。
简而言之,您想将“准备”时刻与“点火”时刻分开。
如果可以扩展构造函数,则它不应自燃。
如果它自燃,那么它就有可能在完全建造之前发射。
毕竟,有朝一日可以在子类的构造函数中添加更多准备工作。而且您无法控制超类的构造函数的执行顺序。
PS:考虑同时实现 Closeable 接口。