方法调用
Java 的方法的执行分为两个部分:
- 方法调用:确定被调用的方法是哪一个;
- 基于栈的解释执行:真正的执行方法的字节码。
在本节中我们将对方法调用进行详细的讲解,我们知道,一切方法的调用在 Class 文件中存储的都是常量池中的符号引用,而不是方法实际运行时的入口地址(直接引用),直到类加载的时候,甚至是实际运行的时候才回去会去确定要被运行的方法的直接引用,而确定要被运行的方法的直接引用的过程就叫做方法调用。
方法调用字节码指令
Java 虚拟机提供了 5 个职责不同的方法调用字节码指令:
invokestatic
:调用静态方法;invokespecial
:调用构造器方法、私有方法、父类方法;invokevirtual
:调用所有虚方法,除了静态方法、构造器方法、私有方法、父类方法、final 方法的其他方法叫虚方法;invokeinterface
:调用接口方法,会在运行时确定一个该接口的实现对象;invokedynamic
:在运行时动态解析出调用点限定符引用的方法,再执行该方法。
除了 invokedynamic
,其他 4 种方法的第一个参数都是被调用的方法的符号引用,是在编译时确定的,所以它们缺乏动态类型语言支持,因为动态类型语言只有在运行期才能确定接收者的类型,即变量的类型检查的主体过程在运行期,而非编译期。
final 方法虽然是通过
invokevirtual
调用的,但是其无法被覆盖,没有其他版本,无需对接收者进行多态选择,或者说多态选择的结果是唯一的,所以属于非虚方法。
解析调用
解析调用,正如其名,就是 在类加载的解析阶段,就确定了方法的调用版本 。我们知道类加载的解析阶段会将一部分符号引用转化为直接引用,这一过程就叫做解析调用。因为是在程序真正运行前就确定了要调用哪一个方法,所以 解析调用能成立的前提就是:方法在程序真正运行前就有一个明确的调用版本了,并且这个调用版本不会在运行期发生改变。
符合这两个要求的只有以下两类方法:
- 通过
invokestatic
调用的方法:静态方法; - 通过
invokespecial
调用的方法:私有方法、构造器方法、父类方法;
这两类方法根本不可能通过继承或者别的方式重写出来其他版本,也就是说,在运行前就可以确定调用版本了,十分适合在类加载阶段就解析好。它们会在类加载的解析阶被解析为直接引用,即确定调用版本。
分派调用
在介绍分派调用前,我们先来介绍一下 Java 所具备的面向对象的 3 个基本特征:封装,继承,多态。
其中多态最基本的体现就是重载和重写了,重载和重写的一个重要特征就是方法名相同,其他各种不同:
- 重载:发生在同一个类中,入参必须不同,返回类型、访问修饰符、抛出的异常都可以不同;
- 重写:发生在子父类中,入参和返回类型必须相同,访问修饰符大于等于被重写的方法,不能抛出新的异常。
相同的方法名实际上给虚拟机的调用带来了困惑,因为虚拟机需要判断,它到底应该调用哪个方法,而这个过程会在分派调用中体现出来。其中:
- 方法重载 —— 静态分派
- 方法重写 —— 动态分派
静态分派(方法重载)
在介绍静态分派前,我们先来介绍一下什么是变量的静态类型和实际类型。
变量的静态类型和实际类型
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.