Java 动态绑定:为什么编译器无法区分重写的方法

Java dynamic binding: Why the compiler cannot distinguish overriden methods

我正在尝试理解 dynamic/static 绑定在更深层次上的作用,我可以说在大量阅读和搜索之后我真的对某些事情感到困惑。

嗯,java 对覆盖的方法使用动态绑定,原因是编译器不知道该方法属于哪个 class,对吧? 例如:

public class Animal{
       void eat(){
}

class Dog extends Animal{
       @Override
       void eat(){}
}

public static void main(String[] args[]){
     Dog d = new Dog();
     d.eat();
}

我的问题是为什么编译器不知道代码引用了 Dog class eat() 方法,即使 d 引用被声明为 class Dog 和 Dog 的构造函数用于在运行时创建实例? 该对象将在运行时创建,但为什么编译器不理解代码引用了 Dog 的方法?这是编译器设计的问题还是我遗漏了什么?

and the reason for this is that the compiler doesn't know to which class the method belongs to, right?

实际上,没有。编译器不想知道目标对象的具体类型。这允许现在编译的代码在将来使用 类 甚至还不存在。

最明显的例子是 JDK 方法,例如 Collections.sort(List)。您可以将刚刚创建的 List 的实现传递给它。您不想通知 Oracle 您做了这件事,并希望他们将其包含在他们的 "statically supported" 列表类型列表中。

动态绑定是绝对必要的。例如,假设您有这样的东西:

Animal a;
String kind = askTheUser();
if (kind.equals("Dog") {
    a = new Dog();
}
else {
    a = new Cat();
}
a.eat();

很明显,编译器在编译时无法知道 a 是狗。它可能是一只猫。所以它必须使用动态绑定。

现在你可以说在你的例子中,编译器可以知道并可以优化。然而,Java 并不是这样设计的。多亏了 JIT 编译器,大部分优化发生在运行时。 JIT 编译器(可能)能够在运行时进行这种优化,以及静态编译器无法做到的更多。 Java 因此决定简化静态编译器和字节码,并将其优化工作集中在 JIT 编译器上。

所以当编译器编译它时,它只关心 d.eat() 行。 d 是 Dog 类型,eat() 是 Dog class 层次结构中存在的可重写方法,生成动态调用该方法的字节码。

不清楚你的问题究竟是基于什么。

当您有以下形式的代码时

 Dog d = new Dog();
 d.eat();

d 的静态类型是 Dog,因此,编译器会在检查调用是正确。

对于调用,有几种可能的场景

  • Dog 可能会声明一个方法 eat(),该方法覆盖其 superclass Animal 中具有相同签名的方法,就像您的示例
  • Dog 可能会声明一个方法 eat() 不会覆盖另一个方法
  • Dog可能没有声明一个匹配方法,但是从它的superclass或实现的接口
  • 继承了一个匹配方法

请注意,适用于哪种场景完全无关紧要。如果调用有效,它将被编译为 Dog.eat() 的调用,而不管应用哪种情况,因为调用 eat()d 的正式静态类型是 Dog.

对实际场景的不可知论也意味着在 运行 时,您可能有不同版本的 class Dog,另一个场景适用于该版本,而不会中断兼容性。


如果写了就不一样了

Animal a = new Dog();
a.eat();

现在 a 的正式类型是 Animal 并且编译器将检查 Animal 是否包含 eat() 的声明,是否在 [=13= 中被覆盖] 或不。然后,此调用将在字节代码中编码为目标 Animal.eat(),即使编译器可以推断出 a 实际上是对 Dog 实例的引用。编译器只是遵循正式规则。这意味着如果 Animal 的 运行time 版本缺少 eat() 方法,即使 Dog 有一个方法,此代码也不会工作。


这意味着删除基 class 中的方法将是一个危险的更改,但您始终可以重构代码,添加更抽象的基 class 并将方法向上移动 class层次结构,不影响与现有代码的兼容性。这是 Java 设计师的目标之一。

因此,也许您编译了上述两个示例之一,之后,您正在 运行 使用较新的库版本编译您的代码,其中类型层次结构为 Animal>Carnivore>DogDog 没有 eat() 的实现,因为最具体实现的自然位置是 Carnivore.eat()。在那种环境下,您的旧代码仍然会 运行 并做正确的事情,没有问题。

进一步注意,即使您重新编译旧代码而不做任何更改,但使用较新的库,它仍将与旧库版本兼容,因为在您的代码中,您永远不会引用新的 Carnivore类型,编译器将使用正式类型,您在代码中使用 AnimalDog,而不记录 Dog 从 [=38 继承方法 eat() 的事实=] 进入编译后的代码,根据上面解释的正式规则。这里没有惊喜。