为了了解静态和动态绑定的实际工作原理?或者编译器和JVM如何识别它们?
让我们看下面的例子,其中Mammal 是一个父类,它有一个方法speak() 和Human 类扩展Mammal,覆盖speak() 方法,然后再次用speak(String language) 重载它。
public class OverridingInternalExample {
private static class Mammal {
public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); }
}
private static class Human extends Mammal {
@Override
public void speak() { System.out.println("Hello"); }
// Valid overload of speak
public void speak(String language) {
if (language.equals("Hindi")) System.out.println("Namaste");
else System.out.println("Hello");
}
@Override
public String toString() { return "Human Class"; }
}
// Code below contains the output and bytecode of the method calls
public static void main(String[] args) {
Mammal anyMammal = new Mammal();
anyMammal.speak(); // Output - ohlllalalalalalaoaoaoa
// 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Mammal humanMammal = new Human();
humanMammal.speak(); // Output - Hello
// 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Human human = new Human();
human.speak(); // Output - Hello
// 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V
human.speak("Hindi"); // Output - Namaste
// 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V
}
}
当我们编译上面的代码并尝试使用javap -verbose OverridingInternalExample查看字节码时,我们可以看到编译器生成一个常量表,它为我提取的程序的每个方法调用和字节码分配整数代码和包含在程序本身中(请参阅每个方法调用下方的 cmets)
通过查看上面的代码,我们可以看到humanMammal.speak()、human.speak() 和human.speak("Hindi") 的字节码完全不同(invokevirtual #4、invokevirtual #7、invokevirtual #9),因为编译器能够区分它们基于参数列表和类引用。因为所有这些都在编译时静态解决,这就是为什么 方法重载 被称为 Static Polymorphism 或 Static Binding。
但是anyMammal.speak() 和humanMammal.speak() 的字节码是相同的(invokevirtual #4),因为根据编译器,这两种方法都是在Mammal 引用上调用的。
那么现在问题来了,如果两个方法调用具有相同的字节码,那么 JVM 是如何知道调用哪个方法的呢?
嗯,答案隐藏在字节码本身中,它是invokevirtual 指令集。 JVM 使用invokevirtual 指令来调用Java 等效的C++ 虚拟方法。在 C++ 中,如果我们想覆盖另一个类中的一个方法,我们需要将其声明为虚拟,但在 Java 中,所有方法默认都是虚拟的,因为我们可以覆盖子类中的每个方法(私有、最终和静态方法除外)。
在 Java 中,每个引用变量都包含两个隐藏指针
- 指向再次保存对象方法的表的指针和指向 Class 对象的指针。例如[speak(), speak(String) 类对象]
- 指向在堆上为该对象的数据分配的内存的指针,例如实例变量的值。
因此,所有对象引用都间接持有对包含该对象所有方法引用的表的引用。 Java 从 C++ 中借鉴了这个概念,这个表被称为虚拟表(vtable)。
vtable 是一个类似数组的结构,其中包含虚拟方法名称及其对数组索引的引用。 JVM 在将类加载到内存时只为每个类创建一个 vtable。
因此,每当 JVM 遇到 invokevirtual 指令集时,它会检查该类的 vtable 中的方法引用并调用特定方法,在我们的例子中是来自对象而不是引用的方法。
因为所有这些都只能在运行时解决,并且在运行时 JVM 知道要调用哪个方法,这就是为什么 方法覆盖 被称为 动态多态性 或简称为多态性或动态绑定。
你可以在我的文章How Does JVM Handle Method Overloading and Overriding Internally阅读更多细节。