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
>Dog
和 Dog
没有 eat()
的实现,因为最具体实现的自然位置是 Carnivore.eat()
。在那种环境下,您的旧代码仍然会 运行 并做正确的事情,没有问题。
进一步注意,即使您重新编译旧代码而不做任何更改,但使用较新的库,它仍将与旧库版本兼容,因为在您的代码中,您永远不会引用新的 Carnivore
类型,编译器将使用正式类型,您在代码中使用 Animal
或 Dog
,而不记录 Dog
从 [=38 继承方法 eat()
的事实=] 进入编译后的代码,根据上面解释的正式规则。这里没有惊喜。
我正在尝试理解 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()
,该方法覆盖其 superclassAnimal
中具有相同签名的方法,就像您的示例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
>Dog
和 Dog
没有 eat()
的实现,因为最具体实现的自然位置是 Carnivore.eat()
。在那种环境下,您的旧代码仍然会 运行 并做正确的事情,没有问题。
进一步注意,即使您重新编译旧代码而不做任何更改,但使用较新的库,它仍将与旧库版本兼容,因为在您的代码中,您永远不会引用新的 Carnivore
类型,编译器将使用正式类型,您在代码中使用 Animal
或 Dog
,而不记录 Dog
从 [=38 继承方法 eat()
的事实=] 进入编译后的代码,根据上面解释的正式规则。这里没有惊喜。