目录
特别备注:
本章节内容我认为比较难理解,对于【编译、运行阶段方法选择分析】这部分内容还不是很理解,需要多次去理解消化todo
1 关于方法调用
- javac编译后Class文件存储方法的符号引用,而不是直接引用(方法在实际运行时内存地址)
- 该特性给Java带来了更强大的动态扩展能力
- 方法调用阶段的任务就是确定被调用方法的版本(即调用哪一个方法),暂不涉及方法内部运行过程
2 方法解析
- 解析,是一种方法调用形式。
- 解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用转变为直接引用,不必延迟到运行期间再去完成
- 解析调用适用于:符合“编译期可知,运行期不可变”的一类方法,即静态方法、私有方法、实例构造器、父类方法、被final 修饰的方法 这五种
回顾方法调用指令:
指令 | 指令功能 |
---|---|
invokevirtual | 用于调用对象的实例方法,根据对象的实际类型进行分派 |
invokeinterface | 用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 |
invokespecial | 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 |
invokestatic | 用于调用类静态方法 |
invokedynamic | 在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法 |
3 方法分派
- Java的多态性,如重载、重写,都是基于方法分派来实现。
- 分派:寻找和确定目标方法的过程
3.1 静态分派
3.1.1 静态分派概述
- 依赖静态类型来决定方法执行版本的分派动作,称为静态分派
- 静态分派的最典型应用表现就是方法重载
- 静态分派发生在编译阶段,因此确定静态分派的动作不是由虚拟机来执行(javac编译器决定)
特别地:
Javac编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一”的,往往只能确定一个“相对更合适的”版本。不举例说明
3.1.2 方法重载分析
分析以下代码:
//这里static作用只是声明为内部类,跟静态没有关系
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 gentleman) {//实际上编译器已经提示该方法不会被执行
System.out.println("hello,gentleman!");
}
public void sayHello(Woman lady) {//实际上编译器已经提示该方法不会被执行
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);
}
}
运行结果:
hello,guy!
hello,guy!
结果分析:
- Human为变量的静态类型(又名外观类型),Man/Woman则为变量的实际类型(又名运行时类型)
- 使用哪个重载版本,取决于传入参数的数量和数据类型【这点明确】
- 编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据
- 由于静态类型在编译期可知,所以在编译阶段,Javac编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)作为调用目标。
3.1.3 静态类型和动态类型
用以下代码解释静态类型和动态类型:
// 实际类型变化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
// 静态类型变化
sr.sayHello((Man) human) sr.sayHello((Woman) human)
代码分析:
- 对象human的实际类型是可变的,编译期间它具有不确定性,到底是Man还是Woman,必须等到程序运行到这行的时候才能确定
- human的静态类型是Human,也可以在使用时(如 sayHello()方法中的强制转型)临时改变这个类型,但这个改变仍是在编译期是可知的,两次sayHello() 方法的调用,在编译期完全可以明确转型的是Man还是Woman。
总结:
静态类型和实际类型在程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么
3.2 动态分派
3.2.1 invokevirtual指令详解
invokevirtual指令的运行时解析过程[4]大致分为以下几步:
1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。 【就是局部表量表中编号0的元素,存储方法接受者的对象引用】
2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
invokevirtual指令执行的第一步就是在运行期确定方法接收者的实际类型,并根据实际类型来选择方法版本,这个过程就是方法重写的本质
3.2.2 动态分派概述
- 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
- 静态分派的最典型应用表现就是方法重写。
- 多态性的根源在于虚方法调用指令invokevirtual的执行逻辑
特别地:
- 字段不存在多态性,因为字段不使用invokevirtual指令。
- 假如子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段
3.2.3 方法重写分析
分析以下代码:
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 say hello
woman say hello
woman say hello
结果分析:
- 静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时产生了不同的行为,所以择调用的方法版本不可能根据静态类型来决定。
- 本质是这两个变量的实际类型不同
3.3 单分派与多分派
关键点:
Java语言是一门静态多分派、动态单分派的语言
- 方法的实际所属对象、参数都是方法的宗量
- 单分派:根据一个宗量对目标方法进行选择
- 多分派:根据多于一个宗量对目标方法进行选择
4 编译、运行阶段方法选择分析
用以下程序例子,分析java是如何选择正确的执行方法
public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father { //son继承了fathre
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
运行结果:
father choose 360
son choose qq
结果分析:
首先看到:在main()里调用了两次hardChoice()方法
一、编译阶段的方法选择
- 首先是编译阶段中编译器的选择过程,也就是静态分派的过程
- 这时候选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360
- 这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向 Father::hardChoice(360)及Father::hardChoice(QQ)方法的符号引用
- (因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。)
二、运行阶段的方法选择
- 看运行阶段中虚拟机的选择过程,也就是动态分派的过程
- 在执行“son.hardChoice(new QQ())”这行代码时,更准确地说,是在执行这行代码所对应的invokevirtual指令时,由于编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的参数“QQ”到底是“腾讯QQ”还是“奇瑞QQ”,因为这时候参数的静态类型、实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有该方法的接受者的实际类型是Father还是Son
- (因为只有一个宗量作为选择依据, 所以Java语言的动态分派属于单分派类型。)