在程序运行时,进行方法调用是最普遍,最频繁的操作
方法调用不等于方法执行:
Class文件的编译过程不包括传统编译中的连接步骤
Class文件中的一切方法调用在Class文件里面存储的都是符号引用,而不是方法在在实际运行时内存布局中的入口地址,即之前的直接引用:
所有方法调用中的目标方法在Class文件里都是一个常量池的引用
在类的加载解析阶段,会将其中的一部分符号引用转化为直接引用:
方法在程序真正执行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的
也就是说,调用目标在程序代码中完成,编译器进行编译时就必须确定下来,这也叫做方法解析
在Java中符合 "编译期可知,运行期不可变" 的方法有两大类:
这两种方法各自的特点决定这两种方法都不可能通过继承或者别的方式重写版本,因此适合在类加载阶段进行解析
非虚方法: 在类加载阶段会把符号引用解析为该方法的直接引用
虚方法: 在类加载阶段不会将符号引用解析为该方法的直接引用
除去以上的非虚方法,其它的方法均为虚方法
public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public static void sayHello(Human guy) { System.out.println("Hello, Guy!"); } public static void sayHello(Man guy) { System.out.println("Hello, Gentleman!"); } public static void sayHello(woman guy) { System.out.println("Hello, Lady!"); } public static void main(String[] args) { Human man = new Man(); Human women = new Woman(); sayHello(man); sayHello(woman); } }
Human man = new Human();
Human
为变量的静态类型
Man
为变量的实际类型
静态类型和实际类型在程序中都会放生变化:
静态类型:
实际类型:
Human human = new Man(); sayHello(man); sayHello((Man)man); // 类型转换,静态类型变化,转型后的静态类型一定是Man man = new woman(); // 实际类型变化,实际类型是不确定的 sayHello(man); sayHello((Woman)man); // 类型转换,静态类型变化
编译器在重载时是通过参数的静态类型而不是实际类型作为判断依据,静态类型在编译期间可以知道:
编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本
静态分派:
静态分派发生在编译阶段,因此确定静态分派的的动作不是由虚拟机执行的,而是由编译器完成的
由于字面量没有显示静态类型,只能通过语言上的规则去理解和推断
public class LiteralTest { public static void sayHello(char arg) { System.out.println("Hello, char!"); } public static void sayHello(int arg) { System.out.println("Hello, int!"); } public static void sayHello(long arg) { System.out.println("Hello, long!"); } public static void sayHello(Character arg) { System.out.println("Hello, Character!"); } public static void main(String[] arg) { sayHello('a'); } }
编译器将重载方法从上向下依次注释,得到不同的输出
如果编译器无法确定要自定转型为哪种类型,会提示类型模糊,拒绝编译
public class LiteralTest { public static void sayHello(String arg) { // 新增重载方法 System.out.println("Hello, String!"); } public static void sayHello(char arg) { System.out.println("Hello, char!"); } public static void sayHello(int arg) { System.out.println("Hello, int!"); } public static void sayHello(long arg) { System.out.println("Hello, long!"); } public static void sayHello(Character arg) { System.out.println("Hello, Character!"); } public static void main(String[] args) { Random r = new Random(); String s = "abc"; int i = 0; sayHello(r.nextInt() % 2 != 0 ? s : 1 ); // 编译错误 sayHello(r.nextInt() % 2 != 0 ? 'a' : false); //编译错误 } }
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 women = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }
这里不是根据静态类型决定的
导致这个现象的额原因 :这两个变量的实际类型不同
Java虚拟机是如何根据实际类型分派方法的执行版本的: 从invokevirtual指令的多态查找过程开始 ,invokevirtual指令运行时解析过程大致分为以下几个步骤:
invokevirtual指令执行的第一步就是在运行时期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上
这种在运行时期根据实际类型确定方法执行版本的分派过程就叫做动态分派
虚拟机概念解析的模式就是静态分派和动态分派,可以理解虚拟机在分派中 "会做什么" 这个问题
虚拟机 "具体是如何做到的" 在各种虚拟机实现上会有差别:
虚方法表中存放着各个方法的实际入口地址:
具有相同签名的方法,在父类,子类的虚方法表中具有一样的索引序号:
这样当类型变换时,仅仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需要的入口地址
方法表一般在类加载阶段的连接阶段进行初始化:
准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕