- 方法调用不等同于方法中的代码被执行,方法调用阶段的唯一作用就是确定被调用方法的版本。
- 在程序调用的时候,方法调用是最普通、最频繁的操作之一。
- 一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。它给Java带来了更加强大的动态拓展能力,但是使得Java方法调用过程变得相对复杂,某些类调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用
1. 解析
- 所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。
- 这种解析能够成立的条件:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。
- 调用目标在程序代码写好、编译器进行编译那一刻就确定下来
。这类方法的调用被称为解析。 - 在Java语言中符合"编译期可知,运行期不可变"这个要求的方法,主要有
静态方法
和私有方法
两大类。前者与类型有直接关联、后者在外部不可访问。它们都不可能通过继承或者别的方式重写出其他版本,所以它们适合在类加载阶段进行解析。
- 总结:非虚方法无法被覆盖,所以在类加载的解析阶段就会把该方法的符号引用变成直接引用。(解析调用一定是静态的过程)
2 分派
- 分派将会解释多态的一些最基本的体现,如"重载"和"重写"在Java虚拟机中是如何实现的。
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 , gentleman!");
}
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.sayHello(man);
sr.sayHello(woman);
}
}
- 对于上面的变量来说,Human称为变量的"静态类型",而Man称为变量的"实际类型"。
- 变量本身的静态类型是不会改变的,并且最终的静态类型是在编译期可知。而实际类型的变化结果在运行期才可确定,编译器在编译的时候不知道一个对象的实际类型是什么。
- 上面的代码
- 对于对象内的方法,使用哪个重载版本,完全取决于传入参数的数量和数据类型。但是虚拟机在重载时是通过静态类型而不是实际类型作为判定依据的。
- 静态类型在编译期可知,所以在编译期通过参数的静态类型就决定了会使用哪个重载版本,因此输出hello,guy!
重载方法匹配优先级
- 重载方法匹配是有优先级的。
- 例如char装箱后转型为父类,如果有多个父类,那将在继承关系中从下往上开始搜索,越接近上层的优先级越低。
package cn.laoniu;
import java.io.Serializable;
public class Reload {
public void sayType(Character arg) {
System.out.println("Character");
}
public void sayType(long arg) {
System.out.println("long");
}
public void sayType(char arg) {
System.out.println("char");
}
public void sayType(char... arg) {
System.out.println("char...");
}
public void sayType(Serializable arg) {
System.out.println("Serializable");
}
public void sayType(int arg) {
System.out.println("int");
}
public void sayType(Object arg) {
System.out.println("Object");
}
public static void main(String[] args) {
new Reload().sayType('a');
}
}
解析与分派并不是二选一的排他关系,他们是在不同层次上去筛选,确定目标方法的过程。例如静态方法会在编译期确定,在类加载期就进行解析。而静态方法也可以有重载版本,选择重载版本的过程也是通过静态分派完成的
2.2 动态分派
- 动态分派和Java多态性的一个重要体现——重写有着密切的关联。
- 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派
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 Woman extends Human{
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
- 这里的调用的方法版本是不可能在根据静态类型来决定的。因为静态类型相同的 man和woman输出的值确实不一样的。
- 那么就是因为它们的实际类型不同,在字节码中,我们发现方法的调用指令从字节码角度来看,无论是指令还是参数都完全一样。但是最终这两句指令的结果却不相同
- 所以解决问题的关键还是在invokevirtual指令入手
- 它的第一步就是在运行期间确定接受者的实际类型,所以两次调用会根据调用者的实际类型来选择方法版本,这个就是重写的本质。
- 总结:这里的方法是虚方法,可以通过重写修改版本。所以调用的指令是invokevirtual,而这个指令第一步要求就是找到该指令操作数指向对象的实际类型,所以会根据实际类型寻找方法调用的版本。(重写)
因为多态性的根源来自于虚方法的调用指令invokevirtual的执行逻辑,所以对于字段来说是没有多态性的。
区别?
- 两者的区别在于方法调用的时机。静态分派在编译期就确定调用哪个方法(方法名、参数类型、返回值类型都确定),主要涉及到方法的重载。而动态分派是在运行运行期间根据实际类型来确定调用哪个方法,主要涉及到方法的重写。
3 单分派与多分派
- Java中静态分派是多分派,动态分派是单分派。Java是一门静态多分派、动态单分派的语言。
- 方法的接收者与方法的参数统称为方法的宗量,根据分配基于多少宗量,可以划分为单分派和多分派两种。
4 虚拟机动态分派的实现
- 动态分派的方法的版本选择 要求 在接受者类型的方法元数据中 搜索合适目标的方法
- 真正运行时不会去频繁反复搜索类型元数据,而是采用在方法区中建立一个虚方法表,使用虚方法表索引来代替元数据查找以提高性能。
- 虚方法表存放着各个方法的实际入口地址,如果某个方法在子类没有被重写,那么子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果重写了,子类虚方法表中的地址就会替换为指向子类实现版本的入口地址。