(三十)分派调用:静态分派和动态分派
Posted shyroke
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(三十)分派调用:静态分派和动态分派相关的知识,希望对你有一定的参考价值。
分派调用
其实分派分为两种,即动态分派和静态分派。我们在了解分派的时候,通常把它们与重写和重载结合到一起。
重载(overload)与静态分派
我们先看一个题:
public class Main { static abstract class Father { } static class Son extends Father { } static class Daughter extends Father { } public void getSex(Daughter daughter) { System.out.println("i am a girl"); } public void getSex(Son son) { System.out.println("i am a boy"); } public void getSex(Father son) { System.out.println("i am a father"); } public static void main(String[] args) { Father son = new Son(); Father daughter = new Daughter(); Main main = new Main(); main.getSex(son); main.getSex(daughter); } }
其实这个栗子就体现了重载。
要是我们在代码里改一下:
main.getSex((Son)son);
就会输出i am a boy
其实这里也体现出了java的静态分派,我们都可以看到main对象已经确认了,那么main在运行main.getSex(son);
时选择方法的时候,到底是选择getSex(Son son)
还是getSex(Father son)
呢?
我们在代码中son的引用类型是Father
,但是它的实际类型却是Son
。
我们再来看看生成的字节码:
字节码里面0-23我们直接跳过,因为0-23对用的代码是
Father son = new Son(); Father daughter = new Daughter(); Main main = new Main();
这里的字节码的作用是创建内存空间,然后把son 、daughter 和main 实例放到第1、2、3个实例变量表Slot中,这里其实还有第0个实例,是this指针,放到的第0个slot中,这个超出了本文要讲解的内容,故跳过。
我们从24看起,aload_x是把刚刚创建的实例放到操作数栈中,然后才能对其操作。后面第26行可以看到:
invokevirtual #50 //Method getSex:(LMain$Father;)
这里相信大家都可以看出,字节码中已经确定了方法的接收者是main和方法的版本是getSex(Father father)
,所以我们在运行代码的时候,会输出i am a father
。
其实java编译器在重载的时候是通过参数的静态类型而不是实际类型来确定使用哪个重载的版本的。所以这里在字节码中,选择了getSex(Father father)
作为调用目标并把这个方法的符号引用写到main方法的几个invokevirtual
指令的参数里面。
所以依赖静态类型来定位方法执行的版本的分派动作成为静态分派。静态分派的典型应用是方法重载,而且静态分派发生在编译期间,因此,静态分派的动作是由编译器发出的。
另外,编译器能确定出方法的重载版本,但在很多的时候,这个版本并不一定是唯一的,比如我把上面的代码改一下:
public class Main { static abstract class Father { } static class Son extends Father { } public void getSex(Son son) { System.out.println("i am a boy"); } public void getSex(Father son) { System.out.println("i am a father"); } public static void main(String[] args) { Son son = new Son(); Main main = new Main(); main.getSex(son); } }
然后再输出:
这是很正常的执行结果,要是我们把getSex(Son son)注释掉,然后再运行试试:
发现,编译器并找不到getSex(Son son)
这个方法,只有作出适当的妥协,把son向上转型为Father,然后选择了getSex(Father son)
方法。
要是我们再把getSex(Father son)
注释掉,会发现:
这里又选择了妥协并向上继续转型成Object。
综上所述:静态分派是选择的最合适的一个方法版本来重载,然而这个版本并不是唯一确定的。我们在写代码的时候,要尽量避免这种情况发生,虽然这似乎能显示出你知识的很渊博,但这并不是一个明智的选择。
重写(override)与动态分派
看完了静态分派,我们再来看看动态分派。动态分派经常与重写紧密联系在一起,那么我们就先来看一个重写的栗子:
public class Main { static class Father { public void say(){ System.out.println("i am fasther"); } } static class Son extends Father { @Override public void say() { System.out.println("i am son"); } } static class Daughter extends Father { @Override public void say() { System.out.println("i am daughter "); } } public static void main(String[] args) { Father son = new Son(); Father daughter = new Daughter(); son.say(); daughter.say(); } }
output:
i am son
i am daughter
相信大家都知道输出结果是什么,三个类都有say()方法,但是虚拟机是怎样知道调用哪个方法的呢? 别急,我们还是按照惯例,看看字节码:
现在相信大家大概都能看懂里面字节码是怎样回事了吧?
我们发现第17行和第21行对应的java代码应该是:
son.say();
daughter.say()
从字节码来看,这两行代码是一样的。调用了同一个类的同一个方法,都是Father.say()
,那为什么他们最后的输出却不一样??
这里的原因其实要从invokevirtual
的多态查找开始说起,invokevirtual
指令运行时的解析过程大概如下:
- 找到操作数栈的栈顶元素所指向的对象的实际类型,记作C
- 如果在类型C中找到与描述符和简单名称都相符的方法,则进行访问权限校验。通过则放回这个方法的直接引用,否则返回
illegalaccesserror
。 - 否则,则按照继承关系从下住上依次对C的父类进行步骤2的查找。
- 如果始终没有找到合适的方法,则跑出
AbstractMethodError
异常。
由于invokevirtual
指令在执行的第一步就对运行的时候的接收者的实际类型进行查找,所以上面两次调用的invokevirtual
指令都能成功找到实际类型的say()方法,然后把类方法的符号引用解析到不同的直接引用上面,这也是重写的体现。
然后这种运行期根据实际类型来判断方法的执行版本的分派过程叫作动态分派。
以上是关于(三十)分派调用:静态分派和动态分派的主要内容,如果未能解决你的问题,请参考以下文章
64.多态性实现机制—静态分派与动态分派(方法解析静态分派动态分派单分派和多分派)