java - Java中的虚拟表和抽象

标签 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();
    }
}

他们问:

What will be printed?

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

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

然而在 Java 中我们得到:

Derived: 0
Derived: 2

为什么?

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

最佳答案

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

  • main()
    • 调用 Derived.<init>() (隐式空元构造函数)
      • 调用 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 中,每个构造函数都必须调用一个父类(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
}

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

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/

相关文章:

java - 无法运行共享 Groovy 库函数

java - 如何更新 map 中的值(如果存在)否则将其插入

c++ - 派生自模板基类的模板构造函数

C++: protected 类构造函数

c# - 抽象类,如何避免代码重复?

C++ 方法 : Non-template class inheriting from an abstract specialized template

java - Android Intent 中 SecondActivity.class 的正确含义?

java - 如何在 kotlin 中将位图转换为字符串,将字符串转换为位图

java - 体育联赛调度算法

javascript - 我如何(并且应该)在 DOM 对象上创建额外的方法?