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

时间:2016-11-04 11:13:02

标签: java jvm javac dynamic-binding dynamic-dispatch

我试图理解更深层杠杆上的动态/静态绑定,我可以说经过大量的阅读和搜索后,我对某些事情感到困惑。

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

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类eat()方法,即使d引用被声明为类Dog和Dog的构造函数用于在运行时创建实例? 该对象将在运行时创建,但为什么编译器不理解代码引用Dog的方法?这是编译器设计的问题还是我错过了什么?

3 个答案:

答案 0 :(得分:3)

  

,原因是编译器不知道该方法属于哪个类,对吧?

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

最明显的例子是考虑像Collections.sort(List)这样的JDK方法。您可以将它刚刚创建的List实现传递给它。您不希望通知Oracle您已执行此操作,并希望它们将其包含在“静态支持”列表类型列表中。

答案 1 :(得分:2)

动态绑定是绝对必要的。例如,让我们说你有这样的事情:

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类层次结构中,并且用于动态调用此方法的字节代码是生成的。

答案 2 :(得分:1)

目前还不清楚你的问题究竟是什么。

当您拥有表格

的代码时
 Dog d = new Dog();
 d.eat();

静态类型dDog,因此,在检查调用是否正确后,编译器会将Dog.eat()的调用编码到类文件中。

对于调用,可能有几种情况

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

请注意,它完全不相关,适用于哪种情况。如果调用有效,则无论应用哪种情况,它都将被编译为Dog.eat()的调用,因为调用d的{​​{1}}的正式静态类型为{ {1}}。

与实际场景不可知也意味着在运行时,您可能拥有另一个场景适用的类eat()的不同版本,而不会破坏兼容性。

如果你写了

,那将是一幅不同的画面
Dog

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

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

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

进一步注意,即使您在没有更改的情况下重新编译旧代码,但使用较新的库,它仍将与旧库版本保持兼容,就像在代码中一样,您永远不会引用新的Dog类型,编译器将使用您在代码eat()Carnivore.eat()中使用的正式类型,而不记录CarnivoreAnimal继承方法Dog的事实根据上面解释的正式规则进入编译的代码。这里没有惊喜。