一、基本概念

Java是一门面向对象的程序设计语言,因为Java具备面向对象的三个基本特征:封装、继承和多态。这三个特征并不是各自独立的,从一定角度上看,封装和继承几乎都是为多态而准备的。多态性主要体现在对象的方法调用上:

1.编译期根据对象的静态类型进行静态分派。

2.运行期根据对象的实际类型进行动态分派。

在进一步解释分派的原理之前,先熟悉几个概念:

1.静态类型和实际类型

  1. Map map = new HashMap();
  2. System.out.println((Object)map);
  3. map = new IdentityHashMap();

上面的第1行代码中,定义了一个变量map,其中‘Map’称为变量的静态类型(Static Type)或外观类型(Apparent Type),‘HashMap’称为变量的实际类型(Actual Type)。

静态类型和实际类型在程序中都可以发生变化,区别是静态类型的变化仅仅在使用时发生变化,并且静态类型是在编译期可知的。而实际类型变化的结果在运行期才能确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。如上面的第2行代码:变量的静态类型改变成了Object,第三行代码:对象的实际类型变成了IdentityHashMap。

这里可能有人会问:为什么编译器在编译程序的时候不知道一个对象的实际类型是什么?像上面的第3行代码中对象的实际类型变成了IdentityHashMap不是一眼就看出来了吗?编译器不是应该能解析出来的吗?如果你有这样的疑问,那么请看下面的代码:

  1. public void type(Map map){
  2. // doSomething
  3. }

这个方法提供一个Map类型的入参,在方法中使用这个map实例做一些事情。那么这个时候,编译器是无法知道map的实际类型的。这个方法在项目中可能被很多地方调用,有的调用方传HashMap,有的调用方传IdentityHashMap,还有的调用方传LinkedHashMap,等等。所以编译器在编译期间是无法确定对象的实际类型的。不过对象的静态类型是始终能确定的,就如上面的这个方法,不管入参传的什么,该对象的静态类型都是Map。

2.方法宗量

方法的接收者与方法的参数统称为方法的宗量。

二、静态分派

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。Java里面的静态分派的具体体现是方法的重载,这里我们不去讨论重载的语法,直接讨论重载的实现。

  1. public class StaticDispatch {
  2. static class Shape{
  3. }
  4.  
  5. static class Circle extends Shape{
  6. }
  7.  
  8. public void draw(Shape shape){
  9. System.out.println("It is shape!");
  10. }
  11.  
  12. public void draw(Circle circle){
  13. System.out.println("It is circle!");
  14. }
  15.  
  16. public static void main(String[] args) {
  17. Shape shape = new Shape();
  18. Shape circle = new Circle();
  19. StaticDispatch staticDispatch =new StaticDispatch();
  20. staticDispatch.draw(shape);
  21. staticDispatch.draw(circle);
  22. }
  23. }

上面的例子,第2-6行代码定义了两个类:一个形状的抽象类Shape,一个具体的形状-圆形类Circle。

第8-14行代码定义了StaticDispatch的两个重载方法draw,根据不同的形状参数(Shape或Circle),打印出不同的形状信息。

第16-22行代码对重载方法进行测试。需要注意的是第18行,变量circle的实际类型是Circle,静态类型是Shape。程序运行的结果为:

  1. It is shape!
  2. It is shape!

上面的结果对于Java有经验的程序员不足为奇,但是对于初学者或多或少会感到疑惑。21行代码的调用参数明明是Circle的实例,怎么打印的信息是“It is shape!”而不是“It is circle!”。为了解释这个现象,我们来看一下这段代码经过编译之后的汇编代码。(这里的汇编和我们一般说的汇编代码有些区别,一般意义上的汇编代码是机器指令的可读形式。这里说的汇编代码是JVM指令的可读形式)。

  1. ALOAD
  2. ALOAD
  3. INVOKEVIRTUAL StaticDispatch.draw (LStaticDispatch$Shape;)V
  4. ALOAD
  5. ALOAD
  6. INVOKEVIRTUAL StaticDispatch.draw (LStaticDispatch$Shape;)V

这里我们把非关注点的代码都省略了,只留下20和21行代码对应的汇编代码。第1-2行将变量槽中的引用对象推送至操作栈,两个对象分别是staticDispatch和shape。第3行的INVOKEVIRTUAL才是方法的真正调用,调用的方法描述符是StaticDispatch.draw (LStaticDispatch$Shape;)V。对应的Java代码方法:public void draw(Shape)。执行方法调用的时候,INVOKEVIRTUAL指令将先前入栈的操作数弹出栈作为方法的参数和接收者,这里的参数是shape,接收者是staticDispatch。这两者的结合也就是我们上面提及的方法宗量。

第4-6行也是类似的逻辑。唯一不同的是第五行压入操作栈参数是circle。可是调用的方法仍然是StaticDispatch.draw (LStaticDispatch$Shape;)V。

由此可见,编译器在编译的时候是只认静态类型,由于变量shape和circle的静态类型都是Shape,所以最终编译后的汇编代码都是调用的同一个方法。这样一来,两次调用都打印出“It is shape!”就解释的通了。这里需要额外注意的一个细节点是,两次方法调用的接收者是一样的,都是staticDispatch。

编译器虽然能确定出方法的重载版本,但是在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。对于上面的例子,变量的静态类型是显式指明的,所以是能唯一确定的,不会存在二义性。但是如果变量的静态类型没有显式指明,那么该怎么去确定方法的执行版本呢?下面的例子演示了编译器如何选择“更加合适的”版本。

  1. public class Overload {
  2. public static void sayHello(Object arg){
  3. System.out.println("Hello Object!");
  4. }
  5.  
  6. public static void sayHello(int arg){
  7. System.out.println("Hello int!");
  8. }
  9.  
  10. public static void sayHello(long arg){
  11. System.out.println("Hello long!");
  12. }
  13.  
  14. public static void sayHello(Character arg){
  15. System.out.println("Hello Character!");
  16. }
  17.  
  18. public static void sayHello(char arg){
  19. System.out.println("Hello char!");
  20. }
  21.  
  22. public static void sayHello(char... arg){
  23. System.out.println("Hello char...!");
  24. }
  25.  
  26. public static void sayHello(Serializable arg){
  27. System.out.println("Hello Serializable!");
  28. }
  29.  
  30. public static void main(String[] args) {
  31. sayHello('a');
  32. }
  33. }

Overload类中定义了一系类的重载方法sayHello,在main函数中进行了调用,传入的参数不是带类型的变量,而是字符字面量'a'。这里并没有显式的指明变量的静态类型,哪个重载的方法是满足的呢?

答案是:每个方法都是满足的。因为你可以说字面量'a'是char型,可以说它是Character型,甚至可以说它是Object型。那么编译器该如何抉择呢?是随机的选择一个方法吗?

答案是:不是的。编译器是根据匹配优先级确定方法的执行版本。

因为'a'最符合char型的定义,所以优先匹配sayHello(char arg)方法。上面的代码执行后将输出"Hello char...!"。

如果把sayHello(char arg)注释掉,会输出什么呢?这个时候编译器会自动将'a'转型为int,将会调用sayHello(int arg),输出"Hello int!"。这是由于'a'除了可以代表一个字符,还可以代表数字97(字符'a'的Unicode值是97)。

如果再把sayHello(int arg)注释掉呢?这里我们就不挨个的去解释了。这些类型匹配的优先级是:char->int->long->float->double->Character->Serializable->Object->char...,读者自行体会。

三、动态分派

了解了静态分派,我们接下来看一下动态分派。所有依赖动态类型来定位方法执行版本的分派动作称为动态分派,动态分派发生在运行期。Java里面的动态分派主要体现在“重写”上。请看下面的例子:

  1. public class DynamicDispatch {
  2. static class Shape {
  3. protected void draw(){
  4. System.out.println("It is shape!");
  5. }
  6. }
  7.  
  8. static class Circle extends Shape {
  9. protected void draw(){
  10. System.out.println("It is circle!");
  11. }
  12. }
  13.  
  14. public static void main(String[] args) {
  15. Shape shape = new Shape();
  16. Shape circle = new Circle();
  17. shape.draw();
  18. circle.draw();
  19. }
  20. }

上面的代码分别创建了两个类Shape和Circle,Circle继承Shape并且重写了draw方法。在main函数中,分别定义了它们的两个实例shape和circle,把两个实例的的静态类型都设置为Shape。那么这次调用它们的draw方法会输出什么呢?

  1. It is shape!
  2. It is circle!

根据上面的结果可以看出,虽然shape和circle的静态类型都是Shape,但是虚拟机在执行的时候并没有傻乎乎的都去执行Shape类中定义的draw方法。为何如此,我们还是来看一下main函数的汇编代码:

  1. NEW DynamicDispatch$Shape
  2. DUP
  3. INVOKESPECIAL DynamicDispatch$Shape.<init> ()V
  4. ASTORE
  5. NEW DynamicDispatch$Circle
  6. DUP
  7. INVOKESPECIAL DynamicDispatch$Circle.<init> ()V
  8. ASTORE
  9. ALOAD
  10. INVOKEVIRTUAL DynamicDispatch$Shape.draw ()V
  11. ALOAD
  12. INVOKEVIRTUAL DynamicDispatch$Shape.draw ()V

代码1-4行是用NEW指令新建Shape实例,并调用实例的初始化方法init(这个就是我们常说的默认构造函数,虽然代码中没写,但是编译器为程序猿自动生成的),实例化完成后,把实例的引用存储在1号变量槽中。第5-8行是类似的逻辑,只是创建的实例不同。这里需要注意的一个细节点是:存储的引用都是实际对象的句柄,就是能通过这个引用找到堆中的实际对象。第9-12行就是两个实例方法的调用,读者可以看到,两次调用的方法描述符都是一样的DynamicDispatch$Shape.draw ()V,不同的地方在于两次调用之前推入操作栈的变量不一样,分别是1号槽和2号槽的变量(也就是shape和circle),这两个变量也就是方法的接收者。问题的关键就在这,在JVM真正去执行方法调用的时候,会去方法的接收者那去寻找方法。所以执行draw方法的时候,shape和circle会去各自的draw方法。

到这里读者应该明白了,虽然编译器按照静态类型生成了方法执行的版本。但是在JVM运行的时候是不看静态类型的,JVM只看方法签名(如上面的draw ()V)和方法的接收者。也就是说:对于一个要调用的方法(比如例子中的draw),最终决定方法执行版本的因素就是方法的宗量。

四、单分派和多分派

前面我们介绍过宗量的概念。根据分派基于多少种宗量,可以将分派划分为单分派和多分派。根据上面静态分派和动态分派的阐述,我们可以知道:

在编译期间,编译器需要根据方法的变量的静态类型和参数才能确定方法的描述符。所以Java的静态分派属于多分派。

在运行期间,方法的名称和描述符已经是确定了的,但是在执行真正的方法调用时,JVM需要根据方法的接收者的实际类型去决定执行的方法版本。所以Java的动态分派属于单分派。

五、虚拟机动态分派的实现

上面介绍动态分派的时候,我们了解到虚拟机是根据实际的方法接收者决定执行方法的版本。那假如方法的接收者对应的类里面没有该方法的定义呢?请看下面的例子。

  1. public class DynamicDispatch {
  2. static class Shape {
  3. protected void draw(){
  4. System.out.println("It is shape!");
  5. }
  6. }
  7.  
  8. static class Circle extends Shape {
  9. }
  10.  
  11. public static void main(String[] args) {
  12. Shape circle = new Circle();
  13. circle.draw();
  14. }
  15. }

代码很简单,类Circle继承Shape,但是没有重写父类的draw方法。main函数中实例化Circle,赋静态类型Shape并调用实例的draw方法。这里的输出相信各位读者都知道:"It is shape!"。这里方法的接收者是circle,虚拟机会先去Circle类里面寻找draw方法,但是Circle类中并没有这个方法,所以虚拟机会向上查找Circle的父类Shape,调用Shape的draw方法。

道理很简单,但是动态分派是个非常频繁的动作,如果每次都这么向上查找的话,会严重影响虚拟机的执行性能。所以虚拟机针对这种情况会做出一些优化手段,最常用的”稳定优化“手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable),使用虚方法表的索引来代替元数据查找以提高性能。

虚拟机会为每个类建立一个虚方法表,如上图左右两个表格分别为Shape和Circle的虚方法表。虚方法表中存放各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的。对于上面的例子,Shape和Circle都默认继承Object,所以它们的vtable里面继承的方法clone、hashCode、equals等都指向Object中对应的方法。这里Circle没有重写父类Shape的draw方法,所以它的vtable中draw方法地址入口指向Shape中的draw方法。如果这里Circle重写了draw方法,它的vtable里面这一项就会指向Circle类的draw方法地址入口。

由于使用了vtable技术,虚拟机在执行动态分派的时候,只需要找到方法接收者所对应的类的虚方法表,就能立即找到实际的方法,不用再向上查找。我们的例子比较简单,只有一个继承层级,真实应用中很可能类存在多个层级,使用vtable技术可以很大程度上提高虚拟机的执行性能。与此对应的,对于接口方法的查找也会用到方法表,只是换了个名字”接口方法表“--Interface Method Table,简称itable。

作者:南唐三少
出处:http://www.cnblogs.com/nantang

如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我们最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文链接,否则保留追究法律责任的权利。

Java多态性——分派的更多相关文章

  1. Java多态性举例说明

    Java多态性的概念也可以被说成“一个接口,多个方法”. (一)相关类 class A ...{ public String show(D obj)...{ return ("A and D ...

  2. java多态性方法的重写Overriding和重载Overloading详解

    java多态性方法的重写Overriding和重载Overloading详解 方法的重写Overriding和重载Overloading是Java多态性的不同表现.重写Overriding是父类与子类 ...

  3. Java多态性的“飘渺之旅”

    原文出处:斯武丶风晴 摘要: 如何从Java多态性进行飘渺之旅呢? 我们用例子来旅行. 朵星人A:人类,是一个很奇妙的物种. 朵星人B:他们好像分为两种,嗯 先生,以及美女? 朵星人C:对,更年轻的有 ...

  4. [转载]深入理解java多态性

    FROM:http://blog.csdn.net/thinkGhoster/article/details/2307001 昨天看到一个关于多态性的帖子,参考了回帖者的理解,加入了一些自己的看法,整 ...

  5. Java多态性详解 (父类引用子类对象)

    面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据. 继承是为了重用父类代码,同时为实现多态性作准备.那么什么是多 ...

  6. Java多态性详解——父类引用子类对象

    来源:http://blog.csdn.net/hikvision_java_gyh/article/details/8957456 面向对象编程有三个特征,即封装.继承和多态. 封装隐藏了类的内部实 ...

  7. java多态性,父类引用指向子类对象

    父类引用指向子类对象指的是: 例如父类Animal,子类Cat,Dog.其中Animal可以是类也可以是接口,Cat和Dog是继承或实现Animal的子类. Animal animal = new C ...

  8. java多态性

    多态分两种: (1)   编译时多态(设计时多态):方法重载. (2)   运行时多态:JAVA运行时系统根据调用该方法的实例的类型来决定选择调用哪个方法则被称为运行时多态.(我们平时说得多的事运行时 ...

  9. Java多态性理解

      Java中多态性的实现 什么是多态 面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 多态的定义:指允许不同类的对 ...

随机推荐

  1. boost强分类器的实现

    boost.cpp文件下: bool CvCascadeBoost::train( const CvFeatureEvaluator* _featureEvaluator, int _numSampl ...

  2. JavaScript 字符串实用常操纪要

    JavaScript 字符串用于存储和处理文本.因此在编写 JS 代码之时她总如影随形,在你处理用户的输入数据的时候,在读取或设置 DOM 对象的属性时,在操作 Cookie 时,在转换各种不同 Da ...

  3. Win10 IIS本地部署网站运行时图片和样式不正常?

    后期会在博客首发更新:http://dnt.dkill.net 异常处理汇总-服 务 器 http://www.cnblogs.com/dunitian/p/4522983.html 启用关闭win功 ...

  4. [BootStrap] 富编辑器,基于wysihtml5

    在我的周围,已经有很多人在使用BootStrap,但对于任何一个带留言.评论.提问.文章编辑功的网站,编辑器永远是重中之重,显然,早期的编辑器完全没考虑过BootStrap的出现,或皮肤跟网站不匹配, ...

  5. iOS之解决崩溃Collection <__NSArrayM: 0xb550c30> was mutated while being enumerated.

    崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CAL ...

  6. 【搬砖】安卓入门(1)- Java开发入门

    01.01_计算机基础知识(计算机概述)(了解) A:什么是计算机?计算机在生活中的应用举例 计算机(Computer)全称:电子计算机,俗称电脑.是一种能够按照程序运行,自动.高速处理海量数据的现代 ...

  7. Android中Fragment的两种创建方式

    fragment是Activity中用户界面的一个行为或者是一部分.你可以在一个单独的Activity上把多个Fragment组合成为一个多区域的UI,并且可以在多个Activity中再使用.你可以认 ...

  8. SQL Server事务、视图和索引

    废话不多说,直接上干货 14:13:23 事务 概括:事务是一种机制,一个操作序列,包含一组数据库操作命令,并且把所有的命令作为一个整体一起 向系统提交或撤销操作 请求. 事务的特性:   1.原子性 ...

  9. [AlwaysOn Availability Groups] 健康模型 Part 2 ——扩展

    健康模型扩展 第一部分已经介绍了AlwayOn健康模型的概述.现在是创建一个自己的PBM策略,然后设置为制定的归类.创建这些策略,创建之后修改一下配置,dashboard就会自动评估这些策略. 场景, ...

  10. Python学习--05函数

    Python函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性,和代码的重复利用率.我们已经知道Python提供了许多内建函数,比如print().但我们 ...