方法调用
方法调用就是确定调用哪一个方法。
在编译阶段,即将java文件转化为class文件,class文件中存储的是方法的符号引用(类似于一个代号吧),而不是直接引用(内存地址),因为类还没有加载到内存嘛,所以具体的内存地址肯定是不知道的。
因此,需要到类加载期间,甚至是运行期间才可能确定目标方法的直接引用。
在类加载的阶段,会将一部分符号引用转化为直接引用,前提是
方法在程序运行前就可以确定他是哪一个。
比如说private和static两类方法,这是因为这两类方法都不可能被继承或者是被重写,只可能有唯一的版本。别的方法就有可能被重写,存在多个版本,难以确定。
除了这两类,还有构造方法以及final方法,这几个都是不可能被重写的,可以唯一确定。
所以,私有方法,final方法,构造方法,static方法在编译期间既可以完全确定,在类加载阶段直接将符号引用转化为直接引用,其他方法都是在运行期间才能确定。
分派
重载和重写在JVM中是如何实现的?
静态分派
重载的实现。
重载时是通过参数的静态类型而不是实际类型决定使用哪个重载函数。
所以,重载的实现需要参数类型或者个数不同。
1 | //Father是静态类型 , Son是实际类型。 |
所以,在编译阶段就可以确定重载的函数是哪一个。
1 | void test() { |
动态分派
重写的实现。
在运行阶段才可以确定重写的函数是哪一个。
1 | public class DynamicDispatch { |
我们从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:
1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。