Java中的虚拟表和摘要

时间:2012-03-04 11:05:07

标签: java inheritance

在一次采访中,我获得了以下代码:

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类的构造函数之前调用。

2 个答案:

答案 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}}。 0x的声明会隐藏Derived中的字段,因此当Base正在打印Derived时,会打印x

编辑:创建Derived.x时的激活顺序:[原理图]

Derived()

第二个是微不足道的,预计[至少在我看来]。