8.3 构造器和多态
通常,构造器不同于其他种类的方法。涉及到多态时仍是如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但改善非常有必要理解构造器怎样通过多态在复杂的层次结构中运作,这一理解将有助于大家避免一些令人不快的困扰。
8.3.1 构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确构造。导出类只能访问它自己的成员,不能访问基类的成员。只有基类构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此必须令所有构造器得到调用,否则就不可能正确构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,它就会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个默认构造器)。
让我们看一个例子,它展示组合,继承以及多态在构建顺序上的作用:
//: polymorphism/Sandwich.java // Order of constructor calls. packagepolymorphism; importstaticnet.mindview.util.Print.*; classMeal { Meal() { print("Meal()"); } } classBread { Bread() { print("Bread()"); } } classCheese { Cheese() { print("Cheese()"); } } classLettuce { Lettuce() { print("Lettuce()"); } } classLunchextendsMeal { Lunch() { print("Lunch()"); } } classPortableLunchextendsLunch { PortableLunch() { print("PortableLunch()");} } publicclassSandwichextendsPortableLunch { privateBreadb=newBread(); privateCheesec=newCheese(); privateLettucel=newLettuce(); publicSandwich() { print("Sandwich()"); } publicstaticvoidmain(String[] args) { newSandwich(); } }
/* Output:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
*///:~
输出结果表明了这一复杂对象调用构造器要遵照下面的顺序:
1、调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最底层的导出类。
2、按声明顺序调用成员的初始化方法。
3、调用导出类构造器的主体。
构造器的调用顺序很重要。当进行继承时,我们已经知道基类的一切,并且可以访问基类中任何声明为public和protected的成员。这意味着导出类中,必须假定基类的所有成员都是有效的。一种标准方法是,构造动作已经发生,那么对象所有部分的全体成员都会得到构建。然而,在构造器内部,我们必须确保所要使用的成员都已经构造完毕。为确保这一目的,唯一的办法就是首先调用基类构造器。那么在进入导出类构造器时,在基类中可供我们访问的成员都已得到初始化。此外,知道构造器中的所有成员都有效是因为,当成员对象在类内进行定义的时候,只要有可能,就应该对他们进行初始化。若遵循这一规则,那么就能保证所有基类成员以及当前对象的成员对象都被初始化了。
8.3.2 继承与清理
通过组合和继承方法来构建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理。如果确实遇到清理问题,那么必须用心为新类创建dispose()方法。并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的dispose()方法时,务必记住调用基类版本dispose()方法;否则,基类的清理动作就不会发生。
//: polymorphism/Frog.java // Cleanup and inheritance. packagepolymorphism; importstaticnet.mindview.util.Print.*; classCharacteristic { privateStrings; Characteristic(Strings) { this.s=s; print("Creating Characteristic "+s); } protectedvoiddispose() { print("disposing Characteristic "+s); } } classDescription { privateStrings; Description(Strings) { this.s=s; print("Creating Description "+s); } protectedvoiddispose() { print("disposing Description "+s); } } classLivingCreature { privateCharacteristicp=newCharacteristic("is alive"); privateDescriptiont=newDescription("Basic Living Creature"); LivingCreature() { print("LivingCreature()"); } protectedvoiddispose() { print("LivingCreature dispose"); t.dispose(); p.dispose(); } } classAnimalextendsLivingCreature { privateCharacteristicp=newCharacteristic("has heart"); privateDescriptiont=newDescription("Animal not Vegetable"); Animal() { print("Animal()"); } protectedvoiddispose() { print("Animal dispose"); t.dispose(); p.dispose(); super.dispose(); } } classAmphibianextendsAnimal { privateCharacteristicp=newCharacteristic("can live in water"); privateDescriptiont=newDescription("Both water and land"); Amphibian() { print("Amphibian()"); } protectedvoiddispose() { print("Amphibian dispose"); t.dispose(); p.dispose(); super.dispose(); } } publicclassFrogextendsAmphibian { privateCharacteristicp=newCharacteristic("Croaks"); privateDescriptiont=newDescription("Eats Bugs"); publicFrog() { print("Frog()"); } protectedvoiddispose() { print("Frog dispose"); t.dispose(); p.dispose(); super.dispose(); } publicstaticvoidmain(String[] args) { Frogfrog=newFrog(); print("Bye!"); frog.dispose(); } }
/* Output:
Creating Characteristic is alive
Creating Description Basic Living Creature
LivingCreature()
Creating Characteristic has heart
Creating Description Animal not Vegetable
Animal()
Creating Characteristic can live in water
Creating Description Both water and land
Amphibian()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both water and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Creature
disposing Characteristic is alive
*///:~
层次结构中的每个类都包含Characteristic 和Description 这两种类型的成员对象,并且他们也必须被销毁。所以万一某个子对象要依赖于其他对象,销毁的顺序应该和初始化顺序相反。
8.3.3 构造器内部的多态方法的行为
构造器调用的层次结构带来一个有趣的两难问题。如果在一个构造器的内部调用正在构造的方法的某个动态绑定方法,那会发生什么情况呢?
在一般的方法内部,动态绑定的调用实在运行时才决定的,因为对象无法知道它属于方法所在的那个类还是属于那个类的导出类。
如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然后这个调用的效果可能相当难以预料,因为被覆盖的方法在对象被完全构造之前就会被调用。这可能会造成一些难于发现的隐藏错误。
从概念上讲,构造器的工作实际上是创建对象。在任何构造器内部,整个对象可能只是部分形成——我们只知道基类对象已经完成初始化。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导出部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次的结构内部,它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化——这肯定会招致灾难。
通过下面的例子,我们会看到问题所在:
// Constructors and polymorphism // don’t produce what you might expect. importstaticnet.mindview.util.Print.*; classGlyph { voiddraw() { print("Glyph.draw()"); } Glyph() { print("Glyph() before draw()"); draw(); print("Glyph() after draw()"); } } classRoundGlyphextendsGlyph { privateintradius=1; RoundGlyph(intr) { radius=r; print("RoundGlyph.RoundGlyph(), radius = "+radius); } voiddraw() { print("RoundGlyph.draw(), radius = "+radius); } } publicclassPolyConstructors { publicstaticvoidmain(String[] args) { newRoundGlyph(5); } }
/* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
从程序输出可以看到,我们可能在构造器中调用其他多态方法时,可能由于赋值问题,导致结果令我们不是很满意。因此,编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。