那么为了了解静态和动态绑定的实际工作原理?或者编译器和JVM如何识别它们?
让我们看下面的例子,Mammal
父类有一个方法speak()
和Human
类 extends 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
时,我们可以看到编译器生成了一个常量表,它为我提取并包含在程序本身中的程序的每个方法调用和字节码分配整数代码(请参阅每个方法调用下面的评论)

通过查看上面的代码,我们可以看到 , 和 的字节码humanMammal.speak()
是human.speak()
完全human.speak("Hindi")
不同的(invokevirtual #4
, invokevirtual #7
, invokevirtual #9
),因为编译器能够根据参数列表和类引用来区分它们。因为所有这些都在编译时静态解决,所以方法重载被称为静态多态性或静态绑定。
anyMammal.speak()
但是和的字节码humanMammal.speak()
是相同的 ( invokevirtual #4
) 因为根据编译器这两种方法都是在Mammal
引用时调用的。
所以现在问题来了,如果两个方法调用具有相同的字节码,那么 JVM 是如何知道调用哪个方法的呢?
好吧,答案隐藏在字节码本身中,它是invokevirtual
指令集。JVM 使用该invokevirtual
指令调用与 C++ 虚拟方法等效的 Java。在 C++ 中,如果我们想覆盖另一个类中的一个方法,我们需要将其声明为虚拟,但在 Java 中,所有方法默认都是虚拟的,因为我们可以覆盖子类中的每个方法(私有、最终和静态方法除外)。
在 Java 中,每个引用变量都包含两个隐藏指针
- 指向再次保存对象方法的表的指针和指向 Class 对象的指针。例如 [speak(), speak(String) 类对象]
- 指向在堆上为该对象的数据分配的内存的指针,例如实例变量的值。
因此,所有对象引用都间接持有对包含该对象的所有方法引用的表的引用。Java 从 C++ 中借用了这个概念,这个表被称为虚拟表(vtable)。
vtable 是一个类似数组的结构,其中包含虚拟方法名称及其对数组索引的引用。JVM 在将类加载到内存时只为每个类创建一个 vtable。
因此,每当 JVM 遇到invokevirtual
指令集时,它都会检查该类的 vtable 中的方法引用并调用特定方法,在我们的例子中,该方法是来自对象而不是引用的方法。
因为所有这些都只在运行时解决,并且在运行时 JVM 知道要调用哪个方法,这就是为什么Method Overriding被称为Dynamic Polymorphism或简称为Polymorphism或Dynamic Binding的原因。
您可以在我的文章How Does JVM Handle Method Overloading and Overriding Internally中阅读更多详细信息。