在一次采访中,我获得了以下代码:
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();
}
}
他们问:
会打印什么?
如果我们使用C ++,我认为代码应该给出编译错误,因为首先调用Derived
构造函数时,会调用Base
类的构造函数。此时foo
方法不存在。
另外我知道在所有之前首先调用继承的类构造函数 变量已创建。
然而,在Java中我们得到:
Derived: 0 Derived: 2
为什么呢?
我知道在C ++中,Java继承始终基于虚拟表,
并且Base
类的构造函数在Derived
类的构造函数之前调用。
答案 0 :(得分:23)
这是执行代码的顺序。更多细节如下。
main()
Derived.<init>()
(隐式的nullary构造函数)
Base.<init>()
Base.x
设置为1
。Derived.foo()
Derived.x
,其默认值仍为0
Derived.x
设置为2
。Derived.foo()
。
Derived.x
,现在为2
。要完全了解正在发生的事情,您需要了解一些事项。
Base
的{{1}}和x
的{{1}}是完全不同的字段,恰好具有相同的名称。 Derived
打印x
,而非Derived.foo
,因为后者被前者“遮蔽”。
由于Derived.x
没有显式构造函数,编译器会生成隐式零参数构造函数。在Java中,每个构造函数都必须调用一个超类构造函数(Base.x
除外,它没有超类),这使超类有机会安全地初始化其字段。编译器生成的nullary构造函数只是调用其超类的nullary构造函数。 (如果超类没有nullary构造函数,则会产生编译错误。)
因此,Derived
的隐式构造函数看起来像
Object
初始化程序块以声明顺序组合,形成一个插入所有构造函数的大块代码。具体来说,它是在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.<init>()
super();
// initializer blocks, in declaration order
x = 1
x = 2
x = 3
// the explicit constructor code
x = 0
}
。如果我们反编译他们的构造函数,我们会看到类似
Base
在Java中,实例方法的调用通常会通过虚方法表。 (也有例外。构造函数,私有方法,final方法和final类的方法都不能被覆盖,因此可以在不通过vtable的情况下调用这些方法。Derived
调用不会通过vtable,因为它们本质上不是多态的。)
每个对象都有一个指向类句柄的指针,该句柄包含一个vtable。一旦分配了对象(使用public Base() {
super(); // Object.<init>()
x = 1; // assigns Base.x
foo();
}
public Derived() {
super(); // Base.<init>()
x = 2; // assigns Derived.x
}
)并且在调用任何构造函数之前,就会设置此指针。所以在Java中,构造函数可以安全地进行虚方法调用,并且它们将被正确地定向到目标的虚方法实现。
因此当super
的构造函数调用NEW
时,它会调用Base
,它会打印foo()
。但尚未分配Derived.foo
,因此会读取并打印默认值Derived.x
。
答案 1 :(得分:10)
显然,只调用派生类的foo()
。
它首次打印0
,因为它在分配x = 2
之前发生了,这只发生在Derived
的构造函数中Base
之后初始化完成了。它打印0
而非1
,因为正在访问Derived.x
而不 Base.x
,并且尚未初始化,并且仍然{ {1}}。 0
中x
的声明会隐藏Derived
中的字段,因此当Base
正在打印Derived
时,会打印x
。
编辑:创建Derived.x
时的激活顺序:[原理图]
Derived()
第二个是微不足道的,预计[至少在我看来]。