示例代码如下:

package com.zhangxf.test;

class BaseClass {
	BaseClass() {
		System.out.println("BaseClass.BaseClass()");
		show();
	}

	void show() {
		System.out.println("BaseClass.show()");
	}
}

public class MyTest extends BaseClass {
	private int x = 10;

	public MyTest(int x) {
		this.x = x;
	}

	void show() {
		System.out.println("MyTest.show(), x = " + x);
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new MyTest(100);
	}

}

以上代码中,关键是BaseClass,在它的构造器中调用了show()方法,现在看这里没有问题。但是注意show()方法的访问控制,它是默认的,也就是这个方法在BaseClass的子类中是可以覆盖的。

MyTest从BaseClass派生,并且重新实现了show()方法。MyTest类的构造器只是给私有成员赋值,其它什么都不做。我们知道MyTest的构建器会默认调用父类也就是BaseClass的构造器。这个时候,BaseClass的构造器会调用自己类内show()实现?还是会调用子类MyTest中的show()实现呢?答案是在C++中,调用的是自己类中定义的show()方法,这个是合理的。因为此时子类正处于构建阶段,它还没有就绪,怎么能够调用子类中的实现呢?另外BaseClass的构建器当然负责构建自己,与子类没什么关系。

但是在Java中,BaseClass调用的确是MyTest中的show()实现,这也就是所谓构造器的多态,构造器的行为取决于子类中的实现。这个明显有问题,就像上例一样,输出结果如下:
BaseClass.BaseClass()
MyTest.show(), x = 0

BaseClass的构建器调用了MyTest中的show()实现,而x的既不是定义它时的值10,也不是new Mytest实例时传入的100,它是0。现在把x的类型改成String,代码如下:

public class MyTest extends BaseClass {
	private String x = "10";

	public MyTest(String x) {
		this.x = x;
	}

	void show() {
		if (x.equals("100")) {
			System.out.println("MyTest.show(), x = " + x);
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new MyTest("100");
	}

}

如果运行这段代码,会报空指针异常。单从这段代码上看完全没有问题,对于show()方法而言,x不可能为null。当然我们现在知道,问题出在父类BaseClass的默认构造函数,但在实际的项目开发中,这种问题很难定位。

为什么x的值是null,不是定义x时给它的值"10",也不是new MyTest("100")中传入的值呢?这个与Java对象创建时的初始化流程有关,如下:

  1. 为对象分配存储空间,并全部初始成二进制0。
  2. 如果有,初始化父类,先初始化其成员,再调用构造器。
  3. 初始化自己的成员
  4. 运行自己的构造器。

以上现象产生的原因就是在第一步中,父类BaseClass调用了子类MyTest中的show()方法,MyTest中的show()方法又用到了自己的成员x,而x这个时候根本没有初始化,它的值是0或者是null。

Java中的构建器多态其实是Java继承、多态实现机制的副产品,实际上并不需要这个特性。在构建函数中应该只做初始化的简单工作,如果要调用方法的话,也应该是final方法,包括private方法,这样就是安全的,因为这两种方法不可能在子类中被重新实现。