静态类型:变量被声明时的类型;例如,Animal a = new Dog(), 静态类型为Animal, 实际类型为Dog
实际类型:变量所引用的对象的真实类型
重载方法是静态分派,即编译时多态
重写方法是动态分派,即运行时多态
单分派和多分派
方法的接收者:一个方法所属的对象
宗量:方法的接收者和方法的参数,只有这两种宗量
单分派:根据一个宗量进行对方法的选择
多分派:根据多于一个的宗量对方法进行选择
单分派和多分派取决于宗量, 方法调用者和方法参数都是宗量.
静态分派的方法调用:首先确定调用者的静态类型是什么,然后根据要调用的方法参数的静态类型(声明类型)确定所有重载方法中要调用哪一个, 需要根据这两个宗量来编译, 所以是静态多分派(多个宗量确定).
动态分派的方法调用:在运行期间,虚拟机会根据调用者的实际类型调用对应的方法, 只需根据这一个宗量就可以确定要调用的方法,所以是动态单分派(一个宗量)
到目前为止,Java 语言还是一门 “静态多分派、动态单分派” 的语言,也就是说在执行静态分派时是根据多个宗量判断调用哪个方法的,因为在静态分派时要根据不同的静态类型和不同的方法描述符选择目标方法,在动态分派的时候,是根据单宗量选择目标方法的,因为在运行期,方法的描述符已经确定好,invokevirtual 字节码指令根据变量的实际类型选择目标方法。
方法的描述符:方法参数类型+返回值类型
静态分派
1 | classStaticDispatch { |
静态类型:是指对象 man 的 Human 类型, 静态类型本身是不会发送变化的,只有在使用时才会发送变化,静态类型在编译期间就可以确定一个变量的静态类型
实际类型:是指对象 man 的 Man 类型,实际类型在编译期间是不可确定的,只有在运行期才可确定
1 | // 实际类型变化 |
所以第一段代码中,方法接收者是 StaticDispatch 对象,虽然两个变量的实际类型不同,但是静态类型是相同的都是 Human,虚拟机(准确的说是编译器)在实现重载时是通过参数的静态类型而不是实际类型做出判定的,并且在编译阶段,变量的静态类型是可以确定的,所以编译器会根据变量的静态类型决定使用哪个重载方法。
所有依赖静态类型定位目标方法的分派动作称为静态分派,静态分派典型的应用就是方法的重载。静态分派发生在编译阶段,所以方法的静态分派动作是由编译器执行的。
动态分派
1 |
|
从上图中,我们可以看到 main() 方法的字节码指令执行过程:
- 0 ~ 7 句是调用 Man 类的实例构造器创建一个 Man 类的对象,并将对象的引用压入到局部变量表的第 1 个 Slot 中
- 8 ~ 15 句是调用 Woman 类的实例构造器创建一个 Woman 类的对象,并将对象的引用压入到局部变量表的第 2 个 Slot 中
- 16 ~ 17 句是将第 1 个 Slot 中的变量(也就是 man)加载到局部变量表中,并调用 sayHello() 方法,关键的就是第 17 句指令 invokevirtual
虽然第 17 句指令调用的常量池中的 Human.sayHello() 方法,但是最终执行的却是 Man.sayHello() 方法,这就要从 invokevirtual 指令的多态查找说起,invokevirtual 的查找过程如下所示:
- 找到操作数栈顶的引用所指的对象的实际类型,记做 C
- 在类型 C 中查找与常量中的描述符和简单名称相同的方法,如果找到则进行访问权限的判断,如果通过则返回这个方法的直接引用,查找结束;如果权限不通过,则返回 java.lang.IllegalAccessError 的异常
- 如果在 C 中没有找到描述符和简单名称都符合的方法,则按照继承关系从下往上依次在 C 的父类中进行查找和验证过程
- 如果最终还是没有找到该方法,则抛出 java.lang.AbstractMethodError 的异常
在上述 invokespecial 查找方法的过程中,最重要的就是第一步,根据对象的引用确定对象的实际类型,这个方法重写的本质。如上所述,在运行期内,根据对象的实际类型确定方法执行版本的分派过程叫做动态分派。
如何理解Java是静态多分派、动态单分派?
编译时期确定方法有两点依据:
- 调用方法的静态类型
- 方法的参数
所以说是静态多分派。
运行时期再确定方法只有一点依据,就是调用方法的实际类型,所以说是动态单分派。
所以最后实际的方法调用是编译和运行的结合,即调用方法的实际类型和参数(运行时期直接引用会根据调用方法的实际类型确定,编译时期调用方法的静态类型只不过是虚引用),按书上说法,在编译时期,方法名和参数就被确定了,运行时只需要确定调用者即可;所以方法选择上,编译时期缩小了范围,运行时期确定了具体的方法。
对于静态分派(重载),肯定会依赖接收者的静态类型与参数的静态类型(参数肯定存在,同名无参的方法只会有一个,不存在分派)。
对于动态分派(重写),虚拟机只会根据接收者的实际类型选择,而不会理睬参数的实际类型。
资料
- 深入理解Java虚拟机(第2版) 第8章 虚拟机字节码执行引擎 8.3.2 分派
- 虚拟机字节码执行引擎(读书笔记)
- 如何理解java是一门静态多分派且动态单分派的语言?