我试图在更深的层次上理解动态/静态绑定,我可以说,经过大量的阅读和搜索,我对一些事情感到非常困惑。
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
类型,编译器将使用形式类型,你在你的代码中使用,Animal
或Dog
,不记录Dog
从Carnivore
继承eat()
方法到编译代码的事实,根据前面解释的形式规则。