#将要理解什么
在这片文章中,我们将了解JVM在内部是如何处理Method-Overloading和Method-Overriding的,以及JVM如何确定哪个方法将被真正的调用 , 将要用到的示例代码如下 :
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 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
}
}
#两种角度
我们可以通过两种不同的角度来理解上述问题 ,逻辑的角度和具体实现的角度 ;
#逻辑的角度
从逻辑上的角度我们可以认为 , 在编译期间 ,被考虑调用的方法是由引用变量的类型决定的,而不是它所指向的实际对象 。不过在程序运行期间,是由引用变量所指向的实际对象所决定的 。
例如 humanMammal.speak() 这行代码 , 编译器会把它编译为预期调用 Mammal.speak() , 因为HumanMammal这个引用变量的类型是 Mammal . 不过在程序的运行期间 , 由于JVM清楚的知道humanMammal所指向的实际对象是Human类型的 ,所以JVM的具体调用并不会沿袭编译时的Mammal.speak() , 而是会调用Human.speak() ;
如此看来 , 我们从逻辑水平(或者说概念性)上来看待这个问题是非常简单明了的 。
不过 ,一旦我们站在实现细节这个角度来看待问题:JVM是如何在内部处理的?或者说JVM是如何决定那个方法被具体调用? , 我们就会产生疑惑, 或者说更进一步的好奇心 ;
正如我们所知的 , Overloaded-Methods并不具有运行时的多态性 ,而只具有编译时的多态性,即由编译器在编译时就决定了那个具体的方法会被调用 ,并且最重要的是 ,JVM在实际执行时,会遵循编译器的结果(这是与Overriding-Methods本质区别)。所以method-Overloading又被称为编译时多态、早期/静态绑定 ;
不过覆写方法(overriden methods)的实际执行,是由JVM在运行期决定的 ,因为编译器并不知道 :我们用引用变量指向的具体对象,到底有没有覆写方法 ;
#具体实现的角度
在这一部分,我们将会站在JVM的具体实现细节上 ,来为上文中的概念性解释提供具体的佐证,并且,我们将通过执行javap -verbose OverridingInternalExample
来获取程序的字节码(使用-verbose
选项可以使我们得到描述性的字节码);
上述的命令将将会把字节码显示为以下两个部分
1. Constant Pool : 这个部分持有着所有一切我们运行Java所必需的东西 , 例如 ,方法引用(Method References , #Methodref) ,Class对象(Class objects , #Class),字符字面值(string literals ,#String) ,如下图所示 :
2. Program's Bytecode : 可执行的字节码指令
为什么方法重载被称为静态绑定(static binding)
对于上文提到的代码humanMammal.speak()
,我们可以由字节码看出 ,编译器在字节码中的预处理是调用Mammal父类的speak()
。但是在JVM的具体执行阶段 ,由于多态性的存在 , JVM并不会遵循编译器的预处理字节码,而是会动态的调用humanMammal实际指向的对象的speak() ,这个对象是Human的实例,而Human是Mammal的子类;
再来对比humanMammal.speak() & human.speak() 和 human.speak("Hindi")
的字节码 ,可以看出前两者和后者是完全不同的,因为编译器在基于class refrence的情况下 ,就能够区别它们是方法覆写还是方法重载。
所以对于Method Overloading(方法重载),我们可以看出 ,编译器在编译阶段,就能准确的识别它所应对应的字节码指令和实际应该调用的方法的地址 ,并且在运行期阶段 ,JVM会遵循编译器的这个预处理 。所以,这就是为什么我们称方法重载为: 静态绑定(static) or 编译期多态(Compile Time Polymorphism)。
为什么方法覆写又被称为动态绑定
针对anyMammal.speak()
, humanMammal.speak()
的字节码都是:invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
; 因为anyMammal和humanMammal都是Mammal类型的引用变量 ,编译器在编译期不会管它实际指向的对象 ,而是会根据引用变量类型来生成预处理字节码(虽然这不会在JVM实际的遵循);
那么问题来了 ,如果两个方法调用具有相同的字节码 , 那么JVM是如何知道到底该调用那个方法 ;
其实,这个答案隐藏在字节码本身的invokevirtual
指令里 ,下面是JVM的官方描述 :
invokevirtual用于在对象上调用实例方法 ,这种调度是基于对象的虚类型。这在Java中是一种正常的方法调度。翻译的貌似有点不通顺 , 下面paster一下英语原话 :invokevirtual invokes an instance method of an object , dispatching on the (virtual) type of the object. This is the normal method dispatch in the Java programming language
由此可见 , JVM使用invokevirtual
指令来调用与Java等价的C++虚方法 ;在C++中 ,如果我们想在子类中覆写一个方法, 我们便需要把它声明为virtual
,不过在Java中,除了final和static方法,所有的方法都默认是virtual
的,这也是为什么我们能在子类中覆写任何方法的原因 ;
c