在一次采访中,我得到了以下代码:
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();
}
}
他们问:
What will be printed?
如果我们使用 C++,我认为代码应该给出编译错误,因为当首先调用 Derived
构造函数时,会调用 Base
类的构造函数。此时 foo
方法不存在。
另外我知道首先调用继承的类构造函数,在所有的 变量已创建。
然而在 Java 中我们得到:
Derived: 0 Derived: 2
为什么?
我知道就像在 C++ 中一样,Java 继承总是基于虚拟表,
并且 Base
类的构造函数在 Derived
类的构造函数之前调用。
最佳答案
这是代码的执行顺序。更多细节如下。
-
main()
- 调用
Derived.<init>()
(隐式空元构造函数)- 调用
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
,因为后者被前者“遮蔽”了。
隐式构造函数
自从 Derived
没有显式构造函数,编译器生成一个隐式零参数构造函数。在 Java 中,每个构造函数都必须调用一个父类(super class)构造函数(Object
除外,它没有父类(super class)),这使父类(super class)有机会安全地初始化其字段。编译器生成的空构造函数只是调用其父类(super class)的空构造函数。 (如果父类(super class)没有空构造函数,则会产生编译错误。)
所以,Derived
的隐式构造函数看起来像
public Derived() {
super();
}
初始化程序 block 和字段定义
初始化程序 block 按声明顺序组合成一大块代码,插入到所有构造函数中。具体来说,它是在 super()
之后插入的。调用但在构造函数的其余部分之前。字段定义中的初始值分配被视为初始化 block 。
如果我们有
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
和 Derived
.如果我们反编译它们的构造函数,我们会看到类似
public Base() {
super(); // Object.<init>()
x = 1; // assigns Base.x
foo();
}
public Derived() {
super(); // Base.<init>()
x = 2; // assigns Derived.x
}
虚拟调用
在 Java 中,实例方法的调用通常通过虚拟方法表。 (这也有异常(exception)。构造函数、私有(private)方法、final 方法和 final 类的方法不能被覆盖,因此这些方法可以在不通过 vtable 的情况下调用。并且 super
调用不通过 vtable,因为它们是本质上不是多态的。)
每个对象都有一个指向类句柄的指针,其中包含一个 vtable。一旦分配了对象(使用 NEW
)并且在调用任何构造函数之前,就会设置此指针。所以在 Java 中,构造函数调用虚方法是安全的,它们会被正确地定向到目标的虚方法实现。
所以当Base
的构造函数调用 foo()
, 它调用 Derived.foo
,打印 Derived.x
.但是Derived.x
还没有赋值,所以默认值0
被阅读和打印。
关于java - Java中的虚拟表和抽象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9554379/