java构造器调用的层次结构带来了一个困境。如果在构造器中调用了正在构造的对象的动态绑定方法,会发生什么呢?
在普通的方法中,动态绑定的调用是在运行时解析的,因为编译期对象不知道它属于方法所在的类还是类的派生类。
如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些 bug 很隐蔽,难以发现。
从概念上讲,构造器的工作就是创建对象,且对象的初始化过程是先基类,再派生类。在构造器内部,整个对象可能只是部分形成——只知道基类对象已经初始化。如果构造器只是构造对象过程中的一个步骤,且构造的对象所属的类是从构造器所属的类派生出的,那么派生部分在当前构造器被调用时还没有初始化。然而,一个动态绑定的方法调用向外深入到继承层次结构中,它可以调用派生类的方法。如果你在构造器中这么做,就可能调用一个方法,该方法操纵的成员可能还没有初始化——这肯定会带来灾难。
class Demo {
void Show() {
System.out.println("Demo Show called");
}
}
class Base {
Base() {
Demo obj = func();
System.out.println("Base constructor called para = " + obj);
obj.Show();
}
Demo func() {
System.out.println("Base func called");
return null;
}
}
class Derived extends Base {
private Demo obj;
Derived() {
System.out.println("Derived constructor called");
obj = new Demo();
}
@Override
Demo func() {
System.out.println("Base func called");
return obj;
}
}
public class App {
public static void main(String[] args) {
Base base = new Derived();
}
}
执行结果:
Base func called
Base constructor called para = null
Exception in thread "main" java.lang.NullPointerException
at Base.<init>(App.java:11)
at Derived.<init>(App.java:23)
at App.main(App.java:37)
这一切,在编译期不会报任何错误。
逻辑方面我们已经做得非常完美,然而行为仍不可思议的错了,编译器也没有报错(C++ 在这种情况下会产生更加合理的行为)。像这样的 bug 很容易被忽略,需要花很长时间才能发现。
因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的 final 方法(这也适用于可被看作是 final 的 private 方法)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。
C++有些不一样:
#include <iostream>
struct TestObj {
int para;
void Show() {
std::cout << "TestObj Show called" << endl; [1]
std::cout << "TestObj Show called" << para << endl; [2]
}
};
struct Base {
Base() {
std::cout << "Base construct called\n";
Func()->Show();
}
virtual TestObj* Func()
{
std::cout << "Base Func called\n";
return 0;
}
};
struct Derived : Base{
Derived() {
std::cout << "Derived construct called\n";
}
TestObj* Func() override
{
std::cout << "Derived Func called\n";
return new TestObj;
}
};
int main()
{
Derived obj;
return 0;
}
[1]不加参数的运行结果
Base construct called
Base Func called
TestObj Show called
Derived construct called
Process finished with exit code 0
[2]加参数的运行结果,崩溃
Base construct called
Base Func called
TestObj Show called
Process finished with exit code -1073741819 (0xC0000005)
- 说明C++的构造函数的虚函数机制,在构造子类之前,虚表中还是基类自己的函数,所以不会崩溃。
- 返回空指针之所以不会崩溃,是因为它并没有操作数据,调用方法不涉及内存的问题,所以不会崩溃,加上数据操作后,程序崩溃。
- 如果将Base中的Func的默认实现去掉,改为纯虚函数,则编译错误。(这点优于java)
网友评论