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

来源:互联网 发布:js正则大于等于0整数 编辑:程序博客网 时间:2024/04/28 17:26
构造器调用的层次结构带来了一个有趣的两难问题。如果在一个构造器的内部,同时调用正
在构造的那个对象的某个动态绑定方法,那会发生什么情况呢?在一般的方法内部,我们可
以想象会发生什么:动态绑定的调用是在运行期才被决定,因为对象无法知道它是属于方法
所在的那个类,还是属于那个类的导出类。为保持一致性,大家也许会认为这应该发生在构
造器内部。


但事情并非完全如此。如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被
重载的定义。然而,产生的效果可能相当难于预料,并且可能造成一些难于发现的隐藏错误。


从概念上讲,构造器的工作实际上是创建对象(这并非是一件平常的工作)。在任何构造器
内部,整个对象可能只有部分形成——我们只知道基类对象已经进行初始化,但却不知道哪
些类是从我们这里继承而来的。然而,一个动态绑定的方法调用却会向外深入到继承层次结
构内部。它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么我们可能会调
用某个方法,而这个方法所操纵的成员可能还未进行初始化——这肯定是招惹灾难的倪端。


通过下面这个例子,我们会看到问题所在:


//: c07:PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
import com.bruceeckel.simpletest.*; 


abstract class Glyph {
abstract void 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 { 
private static Test monitor = new Test(); 
public static void main(String[] args) { 
new RoundGlyph(5);
    monitor.expect(new String[] { 
"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。所以如果忘记为该引用进行初始化,就会在运行期间抛出异常。查看输出结果
时,我们会发现其他所有东西的值都会是零,这通常也正是发现问题的证据。


另一方面,我们应该对这个程序的结果相当震惊。在逻辑方面,我们做的已经十分完美,而
它的行为却不可思议地错了,并且编译器也没有报错。(在这种情况下,C++语言会出现
更合理的行为)。诸如此类的错误会很容易地被人忽略,而且要花很长的时间才能发现。


因此,编写构造器时有一条有益的规则:“用尽可能简单的方法使对象进入正常状态;如果
可以的话,避免调用其他方法”。在构造器内唯一能够安全调用的那些方法是基类中的 final
方法(也适用于 private 方法,它们自动属于 final 方法)。这些方法不能被重载,因此也

就不会出现上述令人惊讶的问题。


原创粉丝点击