那么为了了解静态和动态绑定实际上是如何工作的呢?或如何通过编译器和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
,我们可以看到编译器生成了一个常量表,其中它为我提取并包含在程序本身中的程序的每个方法调用和字节码分配整数代码(参见下面每个方法调用的注释)
通过看上面的代码中我们可以看到的字节码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知道要调用的方法,所以这就是为什么方法覆盖被称为Dynamic Polymorphism或简称为Polymorphism或Dynamic Binding的原因。
您可以在我的文章JVM如何在内部处理方法重载和替代中阅读更多详细信息。