这是执行代码的顺序。更多细节如下。
> main()
>调用Derived。< init>()(隐式nullary构造函数)
>调用Base。< init>()
>将Base.x设置为1。
>调用Derived.foo()
>打印Derived.x,它仍然具有默认值0
>将Derived.x设置为2。
>调用Derived.foo()。
>打印Derived.x,现在是2。
要完全了解发生了什么,您需要知道几件事情。
场阴影
Base的x和Derived的x是完全不同的字段,恰好具有相同的名称。 Derived.foo打印Derived.x,而不是Base.x,因为后者被前者“shadowed”。
隐式构造函数
由于Derived没有明确的构造函数,编译器会生成一个隐式的零参数构造函数。在Java中,每个构造函数必须调用一个超类构造函数(除了Object,它没有超类),这给了超类一个安全地初始化其字段的机会。编译器生成的nullary构造函数只需调用其超类的空值构造函数。 (如果超类没有空值构造函数,则会生成编译错误。)
所以,Derived的隐式构造函数看起来像
public Derived() {
super();
}
初始化程序块和字段定义
初始化程序块以声明顺序组合,形成一个插入到所有构造函数中的大块代码。具体来说,它是在super()调用之后但在构造函数的其余部分之前插入的。字段定义中的初始值分配与初始化程序块类似。
所以如果我们有
class Test {
{x=1;}
int x = 2;
{x=3;}
Test() {
x = 0;
}
}
这相当于
class Test {
int x;
{
x = 1;
x = 2;
x = 3;
}
Test() {
x = 0;
}
}
这就是编译构造函数实际上是如何的:
Test() {
// implicit call to the superclass constructor, Object.()
super();
// initializer blocks, in declaration order
x = 1
x = 2
x = 3
// the explicit constructor code
x = 0
}
现在让我们返回Base和Derived。如果我们反编译它们的构造函数,我们会看到类似的东西
public Base() {
super(); // Object.()
x = 1; // assigns Base.x
foo();
}
public Derived() {
super(); // Base.()
x = 2; // assigns Derived.x
}
虚拟调用
在Java中,实例方法的调用通常通过虚拟方法表。 (这里有例外,最终类的构造方法,私有方法,final方法和方法不能被覆盖,所以这些方法可以被调用而不经过一个vtable,而超级调用不会通过vtables,因为它们本身就不是多态)
每个对象都保存一个指向一个类句柄的指针,它包含一个vtable。一旦对象被分配(使用NEW)并在调用任何构造函数之前,就会设置此指针。所以在Java中,构造函数可以安全地进行虚拟方法调用,并且它们将被正确地定向到目标的虚拟方法的实现。
所以当Base的构造函数调用foo()时,它调用Derived.foo,它打印Derived.x。但是Derived.x尚未分配,因此读取和打印默认值0。