//: polymorphism/Sandwich.java // Order of constructor calls. package polymorphism; import static net.mindview.util.Print.*; class Meal { Meal() { print("Meal()"); } } class Bread { Bread() { print("Bread()"); } } class Cheese { Cheese() { print("Cheese()"); } } class Lettuce { Lettuce() { print("Lettuce()"); } } class Lunch extends Meal { Lunch() { print("Lunch()"); } } class PortableLunch extends Lunch { PortableLunch() { print("PortableLunch()");} } public class Sandwich extends PortableLunch { private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); public Sandwich() { print("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } /* Output: Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich() *///:~
在这个例子中,用其它类创建了一个复杂的类,而且每个类都有一个声明它自己的构造器。其中最重要的类是Sandwich,它反映了三层继承(若将自Object的隐含继承也算在内,就是四层)以及三个成员对象。当在main()里创建一个Sandwich对象后,就可以看到输出结果。这也表明了这一复杂对象调用构造器要遵照下面的顺序:
1)调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,知道最低层的导出类。
2)按声明顺序调用成员的初始化方法。
3)调用导出类构造器的主体。
构造器的调用顺序是很重要的。当进行继承时,我们已经知道基类的一切,并且可以访问基类中声明为public和protected的成员。这意味着在导出类中,必须假定基类的所有成员都是有效的。一种标准方法是,构造动作一经发生,那么对象所有部分的全体成员都会得到构建。然而,在构造器内部,我们必须确保所要使用的成员都已经构建完毕。为确保这一目的,惟一的方法就是首先调用基类构造器。那么在进入导出类构造器时,在基类中可供我们访问的成员都以得到初始化。此外,知道构造器中的所有成员都有效也是因为,当成员对象在类内进行定义的时候(比如上例中的b,c和l),只要有可能,就应该对它们进行初始化(也就是说,通过组合方法将对象置于类内)。若遵循这一规则,那么就能保证所有基类成员以及当前对象的对象都被初始化了。但遗憾的是,这种做法并不适用于所有情况,这一点我们会在下一节中看到。
----------------------------------------------------------------------------
//: polymorphism/PolyConstructors.java // Constructors and polymorphism // don't produce what you might expect. import static net.mindview.util.Print.*; class Glyph { void draw() { print("Glyph.draw()"); } Glyph() { print("Glyph() before draw()"); draw(); print("Glyph() after draw()"); } } class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r; print("RoundGlyph.RoundGlyph(), radius = " + radius); } void draw() { print("RoundGlyph.draw(), radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } /* Output: Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5 *///:~Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,这看起来似乎是我们的目的。但是如果看到输出结果,我们会发现当Glyph的构造器调用draw()方法时,radius不是默认初始值1,而是0.这可能导致在屏幕上只画了一个点,或者根本什么东西都没有;我们只能干瞪眼,并试图找出程序无法运转的原因所在。
前一节讲述的初始化顺序并不十分完整,而这正是解决这一谜题的关键所在。初始化的实际过程是:
1)在其它任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
2)如前所述那样调用基类构造器。此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现radius的值为0.
3)按照声明的顺序调用成员的初始化方法。
4)调用导出类的构造器主体。
这样做有一个优点,那就是所有东西都至少初始化成零(或者是某些特殊数据类型中与"零"等价的值),而不仅仅是留作垃圾。其中包括通过“组合”而嵌入一个类内部的对象引用,其值为null。所以如果忘记为该引用初始化,就会在运行时出现异常。查看输出结果时,会发现其他所有东西的值都会是零,这通常也正是发现问题的证据。