在一次采访中,我得到了以下代码:
public abstract class Base {
public int x = 1;
public Base() {
foo();
}
public abstract void foo();
}
public class Derived extends Base {
int x = 2;
@Override
public void foo() {
System.out.println("Derived: "+x);
}
}
class Main {
public static void main(String... args) {
Base base = new Derived();
base.foo();
}
}
Run Code Online (Sandbox Code Playgroud)
他们问过:
什么会打印?
如果我们使用C++,我认为代码应该给出编译错误,因为Derived首先调用构造函数时,Base会调用类的构造函数.此时该foo方法不存在.
另外我知道在创建所有变量之前,首先调用继承的类构造函数.
但是在Java中我们得到:
Derived: 0 Derived: 2
为什么?
我知道像在C++中一样,Java继承始终基于虚拟表,并且在Base类的构造函数之前调用类的构造函数Derived.
Dan*_*rov 23
这是代码执行的顺序.更多细节如下.
main()
Derived.<init>()(隐式的nullary构造函数)
Base.<init>()
Base.x为1.Derived.foo()
Derived.x,仍然具有默认值0Derived.x为2.Derived.foo().
Derived.x,现在2.要完全了解正在发生的事情,您需要了解一些事项.
Base的x和Derived的x完全碰巧同名不同领域.Derived.foo打印Derived.x,而不是Base.x,因为后者被前者"遮蔽".
由于Derived没有显式构造函数,编译器会生成隐式零参数构造函数.在Java中,每个构造函数都必须调用一个超类构造函数(除了Object没有超类),这使超类有机会安全地初始化其字段.编译器生成的nullary构造函数只是调用其超类的nullary构造函数.(如果超类没有nullary构造函数,则会产生编译错误.)
所以,Derived隐式构造函数看起来像
public Derived() {
super();
}
Run Code Online (Sandbox Code Playgroud)
初始化程序块按声明顺序组合,形成一个插入所有构造函数的大块代码.具体来说,它在super()调用之后但在构造函数的其余部分之前插入.字段定义中的初始值赋值与初始化块一样处理.
所以,如果我们有
class Test {
{x=1;}
int x = 2;
{x=3;}
Test() {
x = 0;
}
}
Run Code Online (Sandbox Code Playgroud)
这相当于
class Test {
int x;
{
x = 1;
x = 2;
x = 3;
}
Test() {
x = 0;
}
}
Run Code Online (Sandbox Code Playgroud)
这就是编译后的构造函数实际上是这样的:
Test() {
// implicit call to the superclass constructor, Object.<init>()
super();
// initializer blocks, in declaration order
x = 1
x = 2
x = 3
// the explicit constructor code
x = 0
}
Run Code Online (Sandbox Code Playgroud)
现在让我们回到Base和Derived.如果我们反编译他们的构造函数,我们会看到类似的东西
public Base() {
super(); // Object.<init>()
x = 1; // assigns Base.x
foo();
}
public Derived() {
super(); // Base.<init>()
x = 2; // assigns Derived.x
}
Run Code Online (Sandbox Code Playgroud)
在Java中,实例方法的调用通常会通过虚方法表.(也有例外.构造函数,私有方法,最终方法和最终类的方法都不能被覆盖,因此可以在不通过vtable的情况下调用这些方法.并且super调用不通过vtable,因为它们本质上不是多态的.)
每个对象都有一个指向类句柄的指针,该句柄包含一个vtable.一旦分配了对象(带NEW)并且在调用任何构造函数之前,就会设置此指针.因此在Java中,构造函数可以安全地进行虚方法调用,并且它们将被正确地定向到目标的虚方法实现.
所以当Base构造函数调用时foo(),它会调用Derived.foo,打印出来Derived.x.但Derived.x尚未分配,因此0读取和打印默认值.
ami*_*mit 10
显然,只foo()调用派生类.
它打印0在第一时间,因为它发生之前分配x = 2,这仅仅发生在构造函数中Derived,后Base的初始化完成.它打印0而不是1,因为Derived.x正在被访问而不是 Base.x,并且它尚未初始化,并且仍然存在0.的声明x中Derived在隐藏了场Base,所以当Derived正在打印x,将打印Derived.x.
编辑:创建时的激活顺序Derived():[原理图]
1. create Base:
1.1. assign Base.x = 1
1.2. invoke foo()
1.2.1 print Derived: Derived.x //Derived.x was not initialized here yet!
2. assign Derived.x = 2
Run Code Online (Sandbox Code Playgroud)
第二个是微不足道的,并且预计[至少在我看来].