Thinking in Java 构造器的调用顺序

//: 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。所以如果忘记为该引用初始化,就会在运行时出现异常。查看输出结果时,会发现其他所有东西的值都会是零,这通常也正是发现问题的证据。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值