Java动态绑定:为什么编译器不能区分被覆盖的方法



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

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

public class Animal{
       void eat(){
}
class Dog extends Animal{
       @Override
       void eat(){}
}
public static void main(String[] args[]){
     Dog d = new Dog();
     d.eat();
}

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

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

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

作为最明显的例子,考虑像Collections.sort(List)这样的JDK方法。您可以将刚刚创建的List的实现传递给它。你不想通知Oracle你做了这个,并希望他们将它包含在"静态支持"列表类型列表中。

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

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类层次结构中的可重写方法,用于动态调用该方法的字节码是生成的。

我不清楚你的问题到底是基于什么。

当你的代码是

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

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

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

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

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

与实际场景无关还意味着,在运行时,您可能有另一个场景应用的Dog类的不同版本,而不会破坏兼容性。


如果你写了

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

现在a的形式类型是Animal,编译器将检查Animal是否包含eat()的声明,是否在Dog中覆盖。尽管编译器可以推断出a实际上是对Dog实例的引用,但这个调用将在字节码中被编码为针对Animal.eat()。编译器只遵循形式规则。这意味着,如果Animal的运行时版本缺少eat()方法,即使Dog有一个,这段代码也不能工作。


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

所以,也许,你编译了上面两个例子中的一个,然后,你用一个新的库版本运行你的代码,其中类型层次结构是Animal> Carnivore> Dog, Dog没有eat()的实现,因为最具体的实现自然是Carnivore.eat()。在这种环境下,你的旧代码仍然会运行并做正确的事情,没有问题。

进一步注意,即使你重新编译你的旧代码没有改变,但使用新的库,它将保持与旧的库版本兼容,因为在你的代码中,你永远不会引用新的Carnivore类型,编译器将使用形式类型,你在你的代码中使用,AnimalDog,不记录DogCarnivore继承eat()方法到编译代码的事实,根据前面解释的形式规则。

相关内容

  • 没有找到相关文章

最新更新