【问题标题】:Java dynamic binding: Why the compiler cannot distinguish overriden methodsJava 动态绑定:为什么编译器无法区分被覆盖的方法
【发布时间】:2016-11-04 11:13:02
【问题描述】:

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

好吧,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();
}

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

【问题讨论】:

  • 它不知道,因为它不在乎。多态性的重点是开发人员不需要知道调用的实现实际上是什么,javac 几乎不做任何优化,它只是验证你的代码。

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


【解决方案1】:

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

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

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

【讨论】:

  • 感谢您的宝贵时间。您所说的“静态支持”究竟是什么意思?
  • 假设没有动态调度,编译器必须知道你的类型。这意味着 Oracle 必须在编译时知道它。我就是这个意思。
  • 不,你根本不需要源代码。您可以编译您的 List 实现而无需听说过 Collections.sort,并且可以在不知道您的代码的情况下编译 Collections.sort。然后,在第三轮中,有人编译使用两个预编译 JAR 的代码,一个使用 Collections.sort,另一个使用您的 List,并使用它们。
  • 请注意,您甚至不需要那些带有编译方法的 JAR,您所需要的只是这些类和方法签名的声明
  • 但是不要忘记,在你的图片中,sort实现必须知道你的类型。您无法控制,甚至没有实现的意识,但是一旦您将对象传递给它,它就会通过动态调度工作。
【解决方案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 类层次结构中的可重写方法,用于动态调用此方法的字节码是生成的。

【讨论】:

    【解决方案3】:

    不清楚你的问题实际上是基于什么。

    当你有表单的代码时

     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 中被覆盖。然后,此调用将在字节码中编码为以Animal.eat() 为目标,即使编译器可以推断出a 实际上是对Dog 实例的引用。编译器只是遵循正式规则。这意味着如果Animal 的运行时版本缺少eat() 方法,即使Dog 有一个,此代码也将无法工作。


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

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

    进一步注意,即使您重新编译旧代码而不做任何更改,但使用较新的库,它将与旧库版本保持兼容,就像在您的代码中一样,您永远不会引用新的 Carnivore 类型和编译器将使用您在代码中使用的形式类型,AnimalDog,而不是根据解释的形式规则记录 DogCarnivore 继承方法 eat() 到编译代码中的事实以上。这里没有惊喜。

    【讨论】:

    • 那么,动态绑定可以为 OOP 设计中的可能变化提供灵活性,对吗?如果 java 只能是静态绑定的,那么唯一的问题就是与未来的变化不兼容班级设计?
    • @Nikos 注意:static 方法在编译时确定。
    • 不,这不只是关于未来的变化。它是编写多态代码的基本能力,例如,它允许您从内存结构的细节中抽象出算法。您只需编写一次sort,它就可以与数组列表、链接列表、数组双端队列等一起使用。
    • @Peter Lawrey:不,使用源代码中编写的类型,即JFrame.getWindows() 编译为invokestatic javax/swing/JFrame.getWindows:()[Ljava/awt/Window;,而不是invokestatic java/awt/Window.getWindows:()[Ljava/awt/Window;。我刚刚验证了它,以确保我的记忆是正确的。原因正如我在回答中所解释的那样,对从未在源代码级别使用过的类的引用并不奇怪。对于接口中static方法的新特性,规则是不同的,即你甚至不允许在源代码中通过子类引用它们。
    • @Holger 感谢您的更正。你又一次教会了我一些新东西。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-13
    • 1970-01-01
    • 2014-09-28
    • 1970-01-01
    • 2017-03-16
    相关资源
    最近更新 更多