Java 中的多态是其三大特性之一, 也是 Java 多变好用的一个优点. 在日常的使用中, 我们经常用到方法重载以及重写, 那么在调用一个方法时, Java 内部是如何从众多重载或重写的方法中选择的呢? 今天来从 JVM 角度来看, 彻底揭开其内部实现原理.
一, 方法调用的介绍
1. 为什么要确定被调用的方法?
方法调用并不等同于方法执行, 方法调用阶段的唯一任务 就是确定被调用方法的版本(即调用哪一个方法), 暂时不涉及方法内部 的具体运行过程. 方法调用在 Class 文件中存储的都只是符号引用, 而不是方法在实际运行时的内存布局的入口地址. 需要在类加载时期, 甚至到运行时期才能确定目标方法的直接引用. 确定被调用的方法可能会发生在类加载时期, 或者运行时期, 确定了被调用方法, 才能执行对应的方法, 获得想要的结果.
2. 确定方法调动的条件?
sr.sayhello(man)
针对上述一个方法调用, 想要确定被调用的方法, 需要确定两个条件, 如下:
1方法的调用者
2方法的版本(即具体是调用者中的哪个方法)
二, 解析调用
解析调用是确定方法的调用者的一种方法. 下面来具体说明什么时候才会发生解析调用.
1. 解析调用发生的条件
所有方法调用中的目标方法在 Class 文件中都是一个常量池中的符号引用, 在类加载的解析阶段, 会将其中的一部分符号引用转换为直接引用. 这种解析能成立的前提是: 方法在程序真正运行之前就有一个可确定的调用版本, 并且这个方法的调用版本在运行期是不改变的.
2. 符合解析调用的主要类
符合解析调用的主要类: 静态方法, 私有方法, 实例构造器等.
这些方法的共性就是: 没有通过继承或别的方式重写其他版本, 通过该方法就可以唯一确定调用该方法的对象所属的类.
解析调用一定是一个静态过程, 在编译期间就完全确定.
不满足解析调用的条件, 那么将会在运行期间确定方法的调用者.
三, 分派调用
分派调用过程是 java 多态的一种基本体现. 能够深入的揭示 "重写" 与 "重载" 在 Java 虚拟机中如何实现的.
分派调用分为两类: 静态分派和动态分派.
其中静态分派的典型应用是方法重载, 用来确定调用的方法的版本.
动态分派的典型应用是方法重写, 用来确定方法的调用者.
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);
- }
- }
输出结果:
- hello,guy
- hello,guy
上述结果表明, 两次方法都调用了 sayHello(Human guy)方法, 为什么两次传入不同的对象, 却调用同一个方法?
这就是静态分派, 我们先从静态类型和实际类型介绍.
Human man = new Man();
对于上述对象 man, 它的静态类型是 Human, 实际类型是 Man.
静态类型是在编译期间确定的类型, 指向的就是引用变量的类型.
实际类型是在运行期间确定的类型, 指向就是通过 new 生成的对象的实际类型.
在方法的调用者 "sr" 已经确定的前提下, 调用哪个版本的方法就由传入的参数来决定. 编译器在重载时是通过参数的静态类型而不是实际类型作为判断依据的. 并且静态类型是编译期间可知的, 因此, 在编译阶段, Javac 编译器会根据参数的静态类型决定使用哪个重载版本, 所以选择了 sayHello(Human guy)作为调用目标.
静态分派调用也发生在编译阶段, 用来定位方法的执行版本的.
2. 动态分派调用
我们就以下列实例来解释动态分派的原理.
- public class DynamicDispatch {
- static abstract class Human{
- protected abstract void sayHello();
- }
- static class Woman extends Human{
- @Override
- protected void sayHello() {
- System.out.println("woman say hello");
- }
- }
- static class Man extends Human{
- @Override
- protected void sayHello() {
- System.out.println("man 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 指向的实际类型进行改变, 执行的结果会发生改变. 由此便可以判断对于重写方法的调用, 是针对实际类型进行调用的.
在经过类加载时期, 没有通过解析调用确定方法的调用者. 那么就会在运行期间, 通过解析确定接收者的实际类型, 然后根据实际类型, 来确定调用重写方法.
总结
解析调用和分派调用并不是二选一的排他关系, 它们是在不同层次上去进行筛选. 解析调用是为了确定方法调用者的类型, 确定了方法调用者的类型后, 方法若存在重载, 那么仍可通过静态分派调用来指定调用哪个版本的方法. 同时, 动态分派和静态分派也是相互协作的. 如果没有通过解析调用确定方法的调用者, 那么会在编译阶段先由静态分派, 决定方法的调用者 (静态类型) 与方法的版本, 然后在运行阶段, 由动态分派来确定实际的方法调用者(实际类型).
自己是从事了七年开发的 Android 工程师, 不少人私下问我, 2019 年 Android 进阶该怎么学, 方法有没有?
没错, 年初我花了一个多月的时间整理出来的学习资料, 希望能帮助那些想进阶提升 Android 开发, 却又不知道怎么进阶学习的朋友.[包括高级 UI, 性能优化, 架构师课程, NDK,Kotlin, 混合式开发(ReactNative+Weex),Flutter 等架构技术资料] , 希望能帮助到您面试前的复习且找到一个好的工作, 也节省大家在网上搜索资料的时间来学习.
来源: http://www.jianshu.com/p/6a02b01fd228