方法调用并不等同于方法执行,方法调用的唯一任务就是确定被调用方法的版本(即调用哪个版本),暂时不涉及方法内部的具体运行过程。因为Class文件的编译过程,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行内存中的入库地址(直接引用),这个特点使得java更具有动态扩展能力。
解析就是所有调用方法在Class文中都是常量池里的一个符号引用,在类加载解析阶段,会将其中一部分符号引用转为直接引用。
只有 方法在程序真正运行之前就有一个确定的调用版本,且这个调用版本是运行期不可变的,这类方法的调用称为解析(Resolution)
在类加载阶段可以直接被解析的有 静态方法 和 私有方法 两大类
虚拟机提供5条方法调用指令:
invokestatic 调用静态方法
invokespecial 调用构造方法,私有方法和父类方法。
invokevirtual 调用虚方法
invokeinterface 调用接口方法,它会在运行的时候确定一个实现该接口的对象
invokedynamic 现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,其他4条指令,分派逻辑是固化在java虚拟机内部的,invokedynamic指令的分派逻辑是由用户所设定的引导方法决定。
invokestatic 和invokespecial 指令调用的方法,在解析阶段可以唯一确定版本,符号条件的有:静态方法、私有方法、实例构造器,父类方法4类,它们在类加载时会吧符号引用解析为直接引用。这些方法称为非虚方法。 除此之外都是虚方法(final方法除外)
解析是一个静态的过程,编译期间即可确定,在解析的时候就能把符号引用直接转化成直接引用。而在java中有一种存在一种方法调用(分派调用),它即可以是静态也可以是动态的,在编译期无法完全确定的情况
分派调用可以是静态的也可以是冬天的,根据分派依据的宗量数(方法的调用者和方法的参数都称为宗量)可分为单分派和多分派。
先看以下的一个案例:
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("human");
}
public void sayHello(Man man){
System.out.println("man");
}
public void sayHello(Woman man){
System.out.println("woman");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
运行结果:
human
human
我们看上述代码中的详细:
Human man = new Man()
其中
Human : 静态类型(Static Type)
Man : 实际类型(Actual Type)
静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是编译期可知的,而实际类型变化的结果在运行期才可确定,编译器在编译程序时并不知道一个对象的实际类型是什么。
// 实际类型变化
Human man = new Man();
man = new Woman();
// 静态类型变化
sr.sayHello((Man)man);
sr.sayHello((Woman)man);
可以总结出虚拟机(准确的说是编译器)在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。
所有依赖静态类型来定位执版本的分配动作都称为静态分配,静态分配的典型案例就是方法重载。静态分派发生在编译阶段,因此确定静态分派在动作实际上不是有虚拟机来执行的。
很多情况下重载的版本并不是唯一的,而是编译器选择一个最适合的版本:
public class StaticPaiDemo {
public static void sayHello(int i){
System.out.println("int 类型");
}
public static void sayHello(Object obj){
System.out.println("obj 类型");
}
public static void sayHello(long i){
System.out.println("long 类型");
}
public static void sayHello(char i){
System.out.println("char 类型");
}
public static void main(String[] args) {
sayHello(1);
sayHello(1L);
sayHello('a');
}
}
执行结果:
int 类型
long 类型
char 类型
看一个重写的案例:
public class DynamicDispatch {
static abstract class Human{
protected abstract void sayHello();
}
static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("man say hello!");
}
}
static class Woman extends Human{
@Override
protected void sayHello() {
System.out.println("woman 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和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?
我们从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:
1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。
我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
java语言是一门静态多分派,动态单分派的语言