示例代码如下:
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对象创建时的初始化流程有关,如下:
- 为对象分配存储空间,并全部初始成二进制0。
- 如果有,初始化父类,先初始化其成员,再调用构造器。
- 初始化自己的成员
- 运行自己的构造器。
以上现象产生的原因就是在第一步中,父类BaseClass调用了子类MyTest中的show()方法,MyTest中的show()方法又用到了自己的成员x,而x这个时候根本没有初始化,它的值是0或者是null。
Java中的构建器多态其实是Java继承、多态实现机制的副产品,实际上并不需要这个特性。在构建函数中应该只做初始化的简单工作,如果要调用方法的话,也应该是final方法,包括private方法,这样就是安全的,因为这两种方法不可能在子类中被重新实现。