注意,方法调用并等同于方法中的代码被执行!方法调用阶段唯一做的事情就是确定被调用方法的版本。
我们知道,Class文件的编译阶段并不像其他传统语言那样会包含连接步骤,在Java语言中,连接步骤是在类的加载过程触发,因此所有的方法调用在Class文件中都只是符号引用,而不是方法在运行过程中实际的内存入口地址(直接引用)。这个特性给Java语言带来了很强大的扩展能力,可以让Java程序在运行过程中"动态"的决定调用哪个方法,但任何事情都有正反两面性,这个特性也让Java的方法调用很复杂。
解析
在类加载的解析阶段,会将一部分符号引用解析为直接引用。这种解析能够成立的前提是:方法在程序运行之前就可以确定调用的版本,并且在运行期间不会改变,即“编译器可知,运行期不可变”。这类方法的调用被称为解析”。
在字节码指令层面,方法调用的指令有以下几种:
1.invokestatic:用于调用静态方法;
2.invokespecial:用于调用实例构造器()方法、私有方法和父类中的方法;
3.invokevirtual:用于调用所有的虚方法;
4.invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象;
5.invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。关于invokedynamic的秘密我们会专门那一章节进行详细探讨。
只要能被invokestatic、invokedspecial这两个指令调用的方法,都可以在类加载的解析阶段确定其唯一的调用版本。符合这个条件的方法类型有:
静态方法:和类直接关联;
私有方法:外部不可访问;
实例构造器;
父类方法;
final修饰的方法:虽然调用final方法的指令是invokevirtual,但是final方法无法被覆盖。因此也可以在类加载的解析阶段确定其唯一版本。
分派
众所周知,Java是面向对象语言,因此Java拥有面向对象的3个基本特征:封装、继承、多态。封装和继承没啥好说的,比较简单。我们重点通过讲解方法的另一种调用方式——分派,在这个过程中揭露Java的多态本质。
静态分派
在讲解静态分派之前,我们先来看这一段代码:
package com.leon.util;
/**
* 静态分派演示
*
* @author created by leon on 2020-05-15
* @since v1.0
*/
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Wuman extends Human {
}
public void sayHello(Human human) {
System.out.println("hello, human.");
}
public void sayHello(Man man) {
System.out.println("hello, man");
}
public void sayHello(Wuman wuman) {
System.out.println("hello, wuman");
}
public static void main(String[] args) {
Human man = new Man();
Human wuman = new