方法调用不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法)。但是Class文件的编译过程不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局的入口地址(相当于直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使方法调用过程变得复杂,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
1. 解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,类加载的解析阶段会将一部分符号引用转化为直接引用,这种解析成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,且这个方法的调用版本在运行期不可变。当然符合这个要求的方法主要包括静态方法和私有方法两大类,前者是与类型直接关联,后者在外部不能被访问,所以他们不会存在其他版本,适合在类加载阶段进行解析。
Java虚拟机提供了5条方法调用字节码指令,分别如下:
- invokestatic:调用静态方法(可在解析时确定方法版本)
- invokespecial:调用实例构造器<init>方法、私有方法和父类方法(可在解析时确定方法版本)
- invokevirtual:调用所有的虚方法
- invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。上面的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分配逻辑是由用户所设定的引导方法决定的。
能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合条件的有静态方法、私有方法、实例构造器、父类方法4类,他们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法,与之相反其他方法称为虚方法(除去final方法,因为它也不会被覆盖也就不会有其他版本)。
解析调用一定是个静态过程,即在编译期间就完全确定,类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到执行期再去完成。而分派调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。所以这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派共四4种分派组合情况。
2. 分派
2.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,guy!");
}
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.