我正在维护一些Java 8代码,如下所示:
Class Entity {
protected Model theModel;
public Entity() {
init();
}
protected void init() {
this.theModel = new Model();
}
}
Class Model {
}
Class SubModel extends Model {
}
main {
Entity newEntity = new Entity() {
@Override
protected void init() {
this.theModel = new SubModel();
}
};
}
代码当前正在编译并正确运行,但我现在需要更新它。
我的问题是:
init()
期间,newEntity
方法的覆盖是如何工作的?到目前为止,我的研究表明Java无法动态覆盖方法 - 在此基础上无法覆盖,因为方法覆盖是按类而不是每个对象。但是这段代码片段似乎表明Java可以在实践中做到这一点吗?
答案 0 :(得分:8)
据我所知,这里没有什么特别的,只是经典constructor chaining和多态应用于虚方法调用。
当你实例化你的匿名类时,它将自动调用它的default constructor(由编译器自动给出),在它的默认构造函数成功之前它必须首先调用它的父类默认构造函数,然后它将调用它init()
方法,由于它已被您的匿名类重写,因此多态,最终调用子类中的init
方法,该方法将模型初始化为SubModel
实例。
约书亚布洛赫在他的着名书籍Effective Java中有一些有趣的论据反对这种模式,在“项目17:设计和继承文件中,或者禁止”他写道:< / p>
“一个班级必须遵守的限制还有一些限制 遗产。构造函数不能调用可覆盖的方法, 直接或间接地。如果违反此规则,程序将失败 结果。超类构造函数在子类之前运行 构造函数,因此将调用子类中的重写方法 在子类构造函数运行之前。如果是压倒一切的方法 取决于子类构造函数执行的任何初始化, 该方法将不会按预期方式运行。为了使这个具体,这里是 违反此规则的类:“
然后他举一个例子,你可以好好学习:
“这是一个覆盖
overrideMe
方法的子类 由Super
唯一的构造函数错误地调用:“public class Super { // Broken - constructor invokes an overridable method public Super() { overrideMe(); } public void overrideMe() { } } public final class Sub extends Super { private final Date date; // Blank final, set by constructor Sub() { date = new Date(); } // Overriding method invoked by superclass constructor @Override public void overrideMe() { System.out.println(date); } public static void main(String[] args) { Sub sub = new Sub(); sub.overrideMe(); } }
“你可能希望这个程序打印两次日期,但它 第一次打印出null,因为
overrideMe
方法是 在Sub
构造函数具有之前由超级构造函数调用 有机会初始化日期字段。请注意,此程序观察到 两个不同状态的最终场!另请注意,如果overrideMe
有 调用date
上的任何方法,调用会抛出一个NullPointerException
构造函数调用Super
时的overrideMe
。 这个程序没有抛出NullPointerException
的唯一原因 它代表的是println
方法有特殊规定 处理空参数。“
所以,正如你所看到的那样,正如Joshua Bloch所解释的那样,风险隐藏在阴影中:你可以在被覆盖的方法中做什么,你有权触摸实例变量那些构造链还没有机会初始化。关键是在构造函数链完全初始化之前,不应该允许您触摸对象状态。
你可能会说在你的特定情况下没有发生,因为你不是非法改变状态而你的被覆盖的方法是受保护的,而不是公开的,但问题是任何接触这个代码的人都需要非常清楚地理解所有这些事情发生在引擎盖下,发生在你当前代码以外的地方。在维护期间,很容易犯一个严重的错误,特别是当你或其他开发人员回到这里进行更改时,可能是在最初定义之后数月甚至数年,并且丢失了所有这些危险的背景,有人引入了一个错误。将很难找到并修复。
答案 1 :(得分:5)
如果它实际上与您向我们展示的完全一样,并且图片中没有重要部分缺失,那么您必须维护的代码很糟糕,并且维护错误的代码非常麻烦。
从构造函数中调用overridable是合法的,但它是 非常糟糕的做法 ,因为可以在尚未调用构造函数的后代上调用可覆盖的, 灾难性的 。在简单的例子中,后代有空构造函数可能并不重要,但是当事情变得更复杂时,它必然会导致重大问题,并且突然有一天后代需要有一个非空的构造函数。
随着时间的推移,事情往往变得更加复杂。
一个体面的IDE会在构造函数中调用overridable时发出一个很大的警告。这反过来意味着代码编写时启用的警告数量不足,这可能意味着它充满了这类问题。
对象构造函数中包含的此方法覆盖的正确术语是: 错误 。
如果没有重大的重构,你无法纠正这个问题。模型需要作为构造函数参数传递,否则构造函数必须承受在构造期间根本无法知道模型的事实。
关于“动态”覆盖方法的问题有点奇怪,可能会使事情变得不必要地复杂化。虚拟方法调度通过虚拟方法表在内部完成。每个类都有自己的虚方法表,永远不会改变。但是,当构造函数执行时,this
指针指向实际(后代)实例,因此有效的虚拟方法表是后代的虚拟方法表。因此,当构造函数调用overridable时,将调用后代的可覆盖。
这与C ++有所不同,其中构造时生效的虚方法表是声明构造函数的类的虚方法表(不管它是否已经被子类化),所以当你从一个虚拟方法中调用一个虚方法时C ++构造函数,你没有调用任何重写方法。