当你在子类中重写val并且在超类的构造器中使用该值的话,其行为并不那么显而易见。有这样一个示例:动物可以感知其周围的环境。简单起见,我们假定动物生活在一维的世界里,而感知数据以整数表示。动物在默认情况下可以看到前方10个单位:
class Creature {
val range : Int=10
val env: Array[Int] = new Array[Int] ( range)
}
不过蚂蚁是近视的:
class Ant extends Creature {
override val range=2
}
面临问题
我们现在面临一个问题:range值在超类的构造器中用到了,而超类的构造器先于子类的构造器运行。确切地说,事情发生的过程是这样的:
1. Ant的构造器在做它自己的构造之前,调用Creature的构造器
2. Creature的构造器将它的range字段设为10
3. Creature的构造器为了初始化env数组,调用range()取值器
4. 该方法被重写以输出(还未初始化的)Ant类的range字段值
5. range方法返回0。这是对象被分配空间时所有整型字段的初始值
6. env被设为长度为0的数组
7. Ant构造器继续执行,将其range字段设为2
虽然range字段看上去可能是10或者2,但env被设成了长度为0的数组。这里的教训是你在构造器内不应该依赖val的值。
解决方案
在Java中,当你在超类的构造方法中调用方法时,会遇到相似的问题。被调用的方法可能被子类重写,因此它可能并不会按照你的预期行事。事实上,这就是我们问题的核心所在range表达式调用了getter方法。有几种解决方式:
1. 将val声明为final。这样很安全但并不灵活
2. 在超类中将val声明为lazy。这样很安全但并不高效
3. 在子类中使用提前定义语法
提前定义语句
所谓的"提前定义"语法,让你可以在超类的构造器执行之前初始化子类的val字段。这个语法简直难看到家了,估计没人会喜欢。你需要将val字段放在位于extends关
键字之后的一个块中,就像这样:
class Ant extends {
override val range=2
} with Creature
注意:超类的类名前的with关键字,这个关键字通常用于指定用到的特质。提前定义的等号右侧只能引用之前已有的提前定义,而不能使用类中的其他字段或方法。
提示:可以用-Xcheckinit编译器标志来调试构造顺序的问题。这个标志会生成相应的代码,以便在有未初始化的字段被访问的时候抛出异常,而不是输出缺省值。
说明:构造顺序问题的根本原因来自Java语言的一个设计决定,即允许在超类的构造方法中调用子类的方法。在C++中,对象的虚函数表的指针在超类构造方法执行的时候被设置成指向超类的虚函数表。之后,才指向子类的虚函数表。因此,在c++中,我们没有办法通过重写修改构造方法的行为。Java设计者们觉得这个细微差别是多余的,Java虚拟机因此在构造过程中并不调整虚拟函数表。