构建器调用的顺序已在第4章进行了简要说明,但那是在继承和多形性问题引入之前说的话。
用于基础类的构建器肯定在一个衍生类的构建器中调用,而且逐渐向上链接,使每个基础类使用的构建器都能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性)。只有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构建器调用的原因。在衍生类的构建器主体中,若我们没有明确指定对一个基础类构建器的调用,它就会“默默”地调用默认构建器。如果不存在默认构建器,编译器就会报告一个错误(若某个类没有构建器,编译器会自动组织一个默认构建器)。
下面让我们看看一个例子,它展示了按构建顺序进行合成、继承以及多形性的效果:
//: Sandwich.java
// Order of constructor calls
class Meal {
Meal() { System.out.println("Meal()"); }
}
class Bread {
Bread() { System.out.println("Bread()"); }
}
class Cheese {
Cheese() { System.out.println("Cheese()"); }
}
class Lettuce {
Lettuce() { System.out.println("Lettuce()"); }
}
class Lunch extends Meal {
Lunch() { System.out.println("Lunch()");}
}
class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch()");
}
}
class Sandwich extends PortableLunch {
Bread b = new Bread();
Cheese c = new Cheese();
Lettuce l = new Lettuce();
Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
} ///:~
这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构建器对自己进行了宣布。其中最重要的类是Sandwich,它反映出了三个级别的继承(若将从Object的默认继承算在内,就是四级)以及三个成员对象。在main()里创建了一个Sandwich对象后,输出结果如下:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
这意味着对于一个复杂的对象,构建器的调用遵照下面的顺序:
(1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
(2) 按声明顺序调用成员初始化模块。
(3) 调用衍生构建器的主体。
构建器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何public和protected成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构建器内部,必须保证使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构建器。然后在进入衍生类构建器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),由于我们应尽可能地对它们进行初始化,所以也应保证构建器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。