Java中的虚拟表和摘要

use*_*968 25 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

他们问过:

什么会打印?

如果我们使用C++,我认为代码应该给出编译错误,因为Derived首先调用构造函数时,Base会调用类的构造函数.此时该foo方法不存在.

另外我知道在创建所有变量之前,首先调用继承的类构造函数.

但是在Java中我们得到:

Derived: 0
Derived: 2

为什么?

我知道像在C++中一样,Java继承始终基于虚拟表,并且在Base类的构造函数之前调用类的构造函数Derived.

Dan*_*rov 23

这是代码执行的顺序.更多细节如下.

  • main()
    • invokes Derived.<init>()(隐式的nullary构造函数)
      • 所调用 Base.<init>()
        • 设置Base.x1.
        • 所调用 Derived.foo()
          • 打印Derived.x,仍然具有默认值0
      • 设置Derived.x2.
    • 调用Derived.foo().
      • 打印Derived.x,现在2.

要完全了解正在发生的事情,您需要了解一些事项.

场阴影

BasexDerivedx完全碰巧同名不同领域.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)

现在让我们回到BaseDerived.如果我们反编译他们的构造函数,我们会看到类似的东西

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读取和打印默认值.

  • 不完全是.`x = 1`发生在`Base()`中的`foo()`之前 (3认同)

ami*_*mit 10

显然,只foo()调用派生类.

它打印0在第一时间,因为它发生之前分配x = 2,这仅仅发生在构造函数中Derived,后Base的初始化完成.它打印0而不是1,因为Derived.x正在被访问而不是 Base.x,并且它尚未初始化,并且仍然存在0.的声明xDerived在隐藏了场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)

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