方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
- 解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用。
静态方法、私有方法、实例构造器和父类方法,这四类方法在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法称为非虚方法,其他方法称为虚方法(除去final方法)。
被final修饰的方法不能被覆盖,没有其他版本,在Java语言规范中明确说明了final方法是一个非虚方法。 - 分派
分派调用过程与多态特性特征的方法“重载”和方法“重写”密切相关。
分派可以分为静态分派、动态分派,还有单分派、多分派的区别。
a、静态分派
静态分派与方法重载密切相关,请看下列代码
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{}
static class Women 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(Women guy){
System.out.println("hello girls");
}
public static void main(String[] args){
Human man = new Man();
Human women = new Women();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man);
sd.sayHello(women);
}
}
这段代码的输出结果为
hello guy!
hello guy!
原因:
我们把代码"Human man =new Man()"中的“Human”称为变量的静态类型,后面的“Man”称为变量的实际类型。静态类型的变化仅仅在使用时发生,实际类型变化的结果在运行期才可确定。虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据。
所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用就是方法重载。
b、动态分派
动态分派与方法重写密切关联。
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 Women extends Human{
@Override
protected void sayHello() {
System.out.print("woman say hello!");
}
}
public static void main(String[] args){
Human man = new Man();
Human women = new Women();
man.sayHello();
women.sayHello();
man = new Women();
man.sayHello();
}
}
以上代码运行结果为
man say hello
woman say hello
woman say hello
代码会把语句
Human man = new Man();
Human women = new Women();
创建的两个对象压到栈顶,在解析过程中会执行以下过程:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C;
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常;
3)否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程;
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
c、虚拟机动态分派实现
动态分派非常频繁,并且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能考虑,会为类在方法区中建立一个虚方法表(对于接口,会建立接口方法表)。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方发表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会被替换为指向子类实现版本的入口地址。