Java中构造器内部的多态方法的行为

  这篇文章主要讨论的是,若在一个构造器中调用正在构造的对象的某个动态绑定的方法时会出现的情况。在此之前,我们需要知道构造器是如何在复杂的层次结构中运作的,尽管构造方法并不具有多态性,因为它们实际上是static方法,只不过是隐式声明的static。

复杂层次结构中构造器的调用顺序

  基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是因为,在Java类中,我们通常将字段设置为private类型,也就是说,在子类中通常无法直接访问基类的字段,那么只有通过调用基类的构造器才能对基类对象的元素进行初始化,那么就必须保证所有的构造器都得到调用,这样才能正确地构造完整的对象。下面的例1展示了包含有组合与继承关系的各类中构造器的调用顺序:
  
例1:

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()");}
}

public class Sandwich extends PortableLunch {
  private Bread b = new Bread();
  private Cheese c = new Cheese();
  private Lettuce l = new Lettuce();
  public Sandwich() { System.out.println("Sandwich()"); }
  public static void main(String[] args) {
    new Sandwich();
  }
}

  例1反映了关于Meal、Lunch和Sandwich之间三层继承关系(不包含Object类),以及Bread、Cheese和Lettuce与Sandwich的组合关系。在main函数中创建一个Sandwich对象后,我们就可以看到输出结果:
  
sandwich1.png-14.5kB

  这说明在复杂的层次结构中构造器的调用遵从的顺序为:
  
  1、调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最低层的导出类;
  2、按声明的顺序调用成员的初始化方法;
  3、调用导出类构造器的主体。
     

构造器内部的多态方法的行为

  那么,现在我们回到文章开头提到的问题,若在一个构造器中调用正在构造的对象的某个动态绑定的方法,会出现什么情况呢?我们知道,动态绑定(或后期绑定)的方法的调用是在运行时才决定的,因为对象在程序运行之前无从得知它自己到底是基类的对象,还是某个导出类的对象。如果在基类的构造器内部调用某个动态绑定方法,该方法是被导出类覆盖的,那么这便可能产生难以预料的后果,因为该导出类的对象还未被完全构造,但它的方法却被调用了。我们可以通过例2看到问题所在:

例2:

class Glyph {
   void draw() { System.out.println("Glyph.draw()"); }
   Glyph() {
     System.out.println("Glyph() before draw()");
     draw();
     System.out.println("Glyph() after draw()");
   }
 }    

 class RoundGlyph extends Glyph {
   private int radius = 1;
   RoundGlyph(int r) {
     radius = r;
     System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
   }
   void draw() {
     System.out.println("RoundGlyph.draw(), radius = " + radius);
   }
 }    

 public class PolyConstructors {
   public static void main(String[] args) {
     new RoundGlyph(5);
   }
 }

运行结果:

sandwich2.png-14.4kB

  在运行结果中,我们看到,基类Glyph的构造器中调用了被子类RoundGlyph覆盖的draw()方法,并且输出了radius=0,这显然是一个错误,因为这个“0”是在其他任何事物发生之前,系统分配给对象的存储空间的初始值——二进制的零,而非我们想要设定的初始值“1”。这是因为,我们在创建子类(RoundGlyph)对象时会先调用基类(Glyph)的构造器构造基类对象,而在基类的构造器中却调用了被子类覆盖的动态绑定的方法(draw()),而这个方法所操纵的可能是子类中的还未进行初始化的成员(radius),这便会招致灾难,尽管编译器并没有报错。
  因此,在编写构造器中有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”。在构造器中,唯一能够安全调用的是基类中的final方法(包括private方法),因为这些方法不能被子类覆盖,也就不会出现上述的问题。

  
参考书籍:《Java编程思想》