Java中的多态是其三大特性之一,也是Java多变好用的一个优点。在日常的使用中,我们经常用到方法重载以及重写,那么在调用一个方法时,Java内部是如何从众多重载或重写的方法中选择的呢?今天来从JVM角度来看,彻底揭开其内部实现原理。
一、方法调用的介绍
1.为什么要确定被调用的方法?
方法调用并不等同于方法执行,方法调用阶段的唯一任务 就是确定被调用方法的版本(即调用哪一个方法),暂时不涉及方法内部 的具体运行过程。方法调用在Class文件中存储的都只是符号引用,而不是方法在实际运行时的内存布局的入口地址。需要在类加载时期,甚至到运行时期才能确定目标方法的直接引用。确定被调用的方法可能会发生在类加载时期,或者运行时期,确定了被调用方法,才能执行对应的方法,获得想要的结果。
2.确定方法调动的条件?
sr.sayhello(man)
针对上述一个方法调用,想要确定被调用的方法,需要确定两个条件,如下:
①方法的调用者
②方法的版本(即具体是调用者中的哪个方法)
二、解析调用
解析调用是确定方法的调用者的一种方法。下面来具体说明什么时候才会发生解析调用。
1.解析调用发生的条件
所有方法调用中的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转换为直接引用。这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不改变的。
2.符合解析调用的主要类
符合解析调用的主要类:静态方法,私有方法,实例构造器等。
这些方法的共性就是:没有通过继承或别的方式重写其他版本,通过该方法就可以唯一确定调用该方法的对象所属的类。
解析调用一定是一个静态过程,在编译期间就完全确定。
不满足解析调用的条件,那么将会在运行期间确定方法的调用者。
三、分派调用
分派调用过程是java多态的一种基本体现。能够深入的揭示“重写”与“重载”在Java虚拟机中如何实现的。
分派调用分为两类:静态分派和动态分派。
其中静态分派的典型应用是方法重载,用来确定调用的方法的版本。
动态分派的典型应用是方法重写,用来确定方法的调用者。
1.静态分派调用
我们就以下列实例来解释静态分派的原理。
public class StaticDispatch {
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public void sayHello(Human guy){
System.out.println("hello,guy");
}
public void sayHello(Man guy){
System.out.println("hello,gentleMan");
}
public void sayHello(Woman guy){
System.out.println("hello,lady");
}
public static void main(String[] args) {
Human man = new Man();
Human woman =new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
输出结果:
hello,guy
hello,guy
上述结果表明,两次方法都调用了sayHello(Human guy)
方法,为什么两次传入不同的对象,却调用同一个方法?
这就是静态分派,我们先从静态类型和实际类型介绍。
Human man = new Man();
对于上述对象man,它的静态类型是Human,实际类型是Man。
静态类型是在编译期间确定的类型,指向的就是引用变量的类型。
实际类型是在运行期间确定的类型,指向就是通过new生成的对象的实际类型。
在方法的调用者“sr”已经确定的前提下,调用哪个版本的方法就由传入的参数来决定。编译器在重载时是通过参数的静态类型而不是实际类型作为判断依据的。并且静态类型是编译期间可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human guy)
作为调用目标。
静态分派调用也发生在编译阶段,用来定位方法的执行版本的。
2.动态分派调用
我们就以下列实例来解释动态分派的原理。
public class DynamicDispatch {
static abstract class Human{
protected abstract void sayHello();
}
static class Woman extends Human{
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman =new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
输出结果:
man say hello
woman say hello
woman say hello
显然,这里对方法的调用不是通过静态类型来进行的。因为静态类型都为Human,却输出不同的结果。并且将man指向的实际类型进行改变,执行的结果会发生改变。由此便可以判断对于重写方法的调用,是针对实际类型进行调用的。
在经过类加载时期,没有通过解析调用确定方法的调用者。那么就会在运行期间,通过解析确定接收者的实际类型,然后根据实际类型,来确定调用重写方法。
总结
解析调用和分派调用并不是二选一的排他关系,它们是在不同层次上去进行筛选。解析调用是为了确定方法调用者的类型,确定了方法调用者的类型后,方法若存在重载,那么仍可通过静态分派调用来指定调用哪个版本的方法。同时,动态分派和静态分派也是相互协作的。如果没有通过解析调用确定方法的调用者,那么会在编译阶段先由静态分派,决定方法的调用者(静态类型)与方法的版本,然后在运行阶段,由动态分派来确定实际的方法调用者(实际类型)。