转自:http://blog.csdn.net/fan2012huan/article/details/51007517

上两篇篇博文讨论了java的重载(overload)与重写(override)、静态分派与动态分派,这篇博文讨论下动态分派的实现方法,即多态override的实现原理。 
java方法调用之重载、重写的调用原理(一) 
java方法调用之单分派与多分派(二)

本文大部分内容来自于IBM的博文多态在 Java 和 C++ 编程语言中的实现比较 。这里写一遍主要是加深自己的理解,方便以后查看,加入了一些自己的见解及行文组织,不是出于商业目的,如若需要下线,请告知。

结论

基于基类的调用和基于接口的调用,从性能上来讲,基于基类的调用性能更高 。因为invokevirtual是基于偏移量的方式来查找方法的,而invokeinterface是基于搜索的。

概述

多态是面向对象程序设计的重要特性。多态允许基类的引用指向派生类的对象,而在具体访问时实现方法的动态绑定。 
java对方法动态绑定的实现方法主要基于方法表,但是这里分两种调用方式invokevirtual和invokeinterface,即类引用调用和接口引用调用。类引用调用只需要修改方法表的指针就可以实现动态绑定(具有相同签名的方法,在父类、子类的方法表中具有相同的索引号),而接口引用调用需要扫描整个方法表才能实现动态绑定(因为,一个类可以实现多个接口,另外一个类可能只实现一个接口,无法具有相同的索引号。这句如果没有看懂,继续往下看,会有例子。写到这里,感觉自己看书时,有的时候也会不理解,看不懂,思考一段时间,还是不明白,做个标记,继续阅读吧,然后回头再看,可能就豁然开朗。)。 
类引用调用的大致过程为:java编译器将java源代码编译成class文件,在编译过程中,会根据静态类型将调用的符号引用写到class文件中。在执行时,JVM根据class文件找到调用方法的符号引用,然后在静态类型的方法表中找到偏移量,然后根据this指针确定对象的实际类型,使用实际类型的方法表,偏移量跟静态类型中方法表的偏移量一样,如果在实际类型的方法表中找到该方法,则直接调用,否则,按照继承关系从下往上搜索。 
下面对上面的描述做具体的分析讨论。

JVM的运行时结构

 
从上图可以看出,当程序运行时,需要某个类时,类载入子系统会将相应的class文件载入到JVM中,并在内部建立该类的类型信息,这个类型信息其实就是class文件在JVM中存储的一种数据结构,他包含着java类定义的所有信息,包括方法代码,类变量、成员变量、以及本博文要重点讨论的方法表。这个类型信息就存储在方法区。 
注意,这个方法区中的类型信息跟在堆中存放的class对象是不同的。在方法区中,这个class的类型信息只有唯一的实例(所以是各个线程共享的内存区域),而在堆中可以有多个该class对象。可以通过堆中的class对象访问到方法区中类型信息。就像在java反射机制那样,通过class对象可以访问到该类的所有信息一样。 
方法表是实现动态调用的核心。方法表存放在方法区中的类型信息中。方法表中存放有该类定义的所有方法及指向方法代码的指针。这些方法中包括从父类继承的所有方法以及自身重写(override)的方法。

类引用调用invokevirtual

代码如下:

package org.fan.learn.methodTable;/*** Created by fan on 2016/3/30.*/
public class ClassReference {static class Person {@Overridepublic String toString(){return "I'm a person.";}public void eat(){System.out.println("Person eat");}public void speak(){System.out.println("Person speak");}}static class Boy extends Person{@Overridepublic String toString(){return "I'm a boy";}@Overridepublic void speak(){System.out.println("Boy speak");}public void fight(){System.out.println("Boy fight");}}static class Girl extends Person{@Overridepublic String toString(){return "I'm a girl";}@Overridepublic void speak(){System.out.println("Girl speak");}public void sing(){System.out.println("Girl sing");}}public static void main(String[] args) {Person boy = new Boy();Person girl = new Girl();System.out.println(boy);boy.eat();boy.speak();//boy.fight();System.out.println(girl);girl.eat();girl.speak();//girl.sing();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

注意,boy.fight(); 和 girl.sing(); 这两个是有问题的,在IDEA中会提示“Cannot resolve method ‘fight()’”。因为,方法的调用是有静态类型检查的,而boy和girl的静态类型都是Person类型的,在Person中没有fight方法和sing方法。因此,会报错。 
执行结果如下: 
 
从上图可以看到,boy.eat() 和 girl.eat() 调用产生的输出都是”Person eat”,因为Boy和Girl中没有override 父类的eat方法。 
字节码指令:

public static void main(java.lang.String[]);Code:Stack=2, Locals=3, Args_size=10:   new     #2; //class ClassReference$Boy3:   dup4:   invokespecial   #3; //Method ClassReference$Boy."<init>":()V7:   astore_18:   new     #4; //class ClassReference$Girl11:  dup12:  invokespecial   #5; //Method ClassReference$Girl."<init>":()V15:  astore_216:  getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;19:  aload_120:  invokevirtual   #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V23:  aload_124:  invokevirtual   #8; //Method ClassReference$Person.eat:()V27:  aload_128:  invokevirtual   #9; //Method ClassReference$Person.speak:()V31:  getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;34:  aload_235:  invokevirtual   #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V38:  aload_239:  invokevirtual   #8; //Method ClassReference$Person.eat:()V42:  aload_243:  invokevirtual   #9; //Method ClassReference$Person.speak:()V46:  return
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

其中所有的invokevirtual调用的都是Person类中的方法。

下面看看java对象的内存模型: 
 
从上图可以清楚地看到调用方法的指针指向。而且可以看出相同签名的方法在方法表中的偏移量是一样的。这个偏移量只是说Boy方法表中的继承自Object类的方法、继承自Person类的方法的偏移量与Person类中的相同方法的偏移量是一样的,与Girl是没有任何关系的。

下面再看看调用过程,以girl.speak() 方法的调用为例。在我的字节码中,这条指令对应43: invokevirtual #9; //Method ClassReference$Person.speak:()V ,为了便于使用IBM的图,这里采用跟IBM一致的符号引用:invokevirtual #12; 。调用过程图如下所示: 
 
(1)在常量池中找到方法调用的符号引用 
(2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。 
(3)根据this指针确定方法接收者(girl)的实际类型 
(4)根据对象的实际类型得到该实际类型对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用;如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。

接口引用调用invokeinterface

代码如下:

package org.fan.learn.methodTable;/*** Created by fan on 2016/3/29.*/
public class InterfaceReference {interface IDance {void dance();}static class Person {@Overridepublic String toString() {return "I'm a person";}public void speak() {System.out.println("Person speak");}public void eat() {System.out.println("Person eat");}}static class Dancer extends Person implements IDance {@Overridepublic String toString() {return "I'm a Dancer";}@Overridepublic void speak() {System.out.println("Dancer speak");}public void dance() {System.out.println("Dancer dance");}}static class Snake implements IDance {@Overridepublic String toString() {return "I'm a Snake";}public void dance() {System.out.println("Snake dance");}}public static void main(String[] args) {IDance dancer = new Dancer();System.out.println(dancer);dancer.dance();//dancer.speak();//dancer.eat();IDance snake = new Snake();System.out.println(snake);snake.dance();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

上面的代码中dancer.speak(); dancer.eat(); 这两句同样不能调用。 
执行结果如下所示: 
 
其字节码指令如下所示:

public static void main(java.lang.String[]);Code:Stack=2, Locals=3, Args_size=10:   new     #2; //class InterfaceReference$Dancer3:   dup4:   invokespecial   #3; //Method InterfaceReference$Dancer."<init>":()V7:   astore_18:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;11:  aload_112:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V15:  aload_116:  invokeinterface #6,  1; //InterfaceMethod InterfaceReference$IDance.dance:()V21:  new     #7; //class InterfaceReference$Snake24:  dup25:  invokespecial   #8; //Method InterfaceReference$Snake."<init>":()V28:  astore_229:  getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;32:  aload_233:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V36:  aload_237:  invokeinterface #6,  1; //InterfaceMethod InterfaceReference$IDance.dance:()V42:  return
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

从上面的字节码指令可以看到,dancer.dance(); 和snake.dance(); 的字节码指令都是invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V 。 
为什么invokeinterface指令会有两个参数呢?

对象的内存模型如下所示: 
 
从上图可以看到IDance接口中的方法dance()在Dancer类的方法表中的偏移量跟在Snake类的方法表中的偏移量是不一样的,因此无法仅根据偏移量来进行方法的调用。(这句话在理解时,要注意,只是为了强调invokeinterface在查找方法时不再是基于偏移量来实现的,而是基于搜索的方式。)应该这么说,dance方法在IDance方法表(如果有的话)中的偏移量与在Dancer方法表中的偏移量是不一样的。 
因此,要在Dancer的方法表中找到dance方法,必须搜索Dancer的整个方法表。

下面写一个,如果Dancer中没有重写(override)toString方法,会发生什么? 
代码如下:

package org.fan.learn.methodTable;/*** Created by fan on 2016/3/29.*/
public class InterfaceReference {interface IDance {void dance();}static class Person {@Overridepublic String toString() {return "I'm a person";}public void speak() {System.out.println("Person speak");}public void eat() {System.out.println("Person eat");}}static class Dancer extends Person implements IDance {//        @Override
//        public String toString() {//            return "I'm a Dancer";
//        }@Overridepublic void speak() {System.out.println("Dancer speak");}public void dance() {System.out.println("Dancer dance");}}static class Snake implements IDance {@Overridepublic String toString() {return "I'm a Snake";}public void dance() {System.out.println("Snake dance");}}public static void main(String[] args) {IDance dancer = new Dancer();System.out.println(dancer);dancer.dance();//dancer.speak();//dancer.eat();IDance snake = new Snake();System.out.println(snake);snake.dance();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

执行结果如下: 
 
可以看到System.out.println(dancer); 调用的是Person的toString方法。 
内存模型如下所示: 

结束语

这篇博文讨论了invokevirtual和invokeinterface的内部实现的区别,以及override的实现原理。下一步,打算讨论下invokevirtual的具体实现细节,如:如何实现符号引用到直接引用的转换的?可能会看下OpenJDK底层的C++实现。

参考资料

  • 周志明 《深入理解JAVA虚拟机》
  • IBM 多态在 Java 和 C++ 编程语言中的实现比较

java方法调用之动态调用多态(重写override)的实现原理——方法表相关推荐

  1. VB静态调用与动态调用dll详解

    [[请注意]]:在以下语法格式中,请注意 [函数名] 的[大小写]!!! 静态与动态比较: 静态调用简单,动态调用麻烦:静态调用占用资源多,动态调用占用资源少:正所谓鱼和熊掌不可兼得. 静态调用定义: ...

  2. 【VB技巧】VB静态调用与动态调用dll详解

    [[请注意]]:在以下语法格式中,请注意 [函数名] 的[大小写]!!!静态与动态比较:静态调用简单,动态调用麻烦:静态调用占用资源多,动态调用占用资源少:正所谓鱼和熊掌不可兼得.静态调用定义:就是常 ...

  3. JAVA正确地自定义比较对象---如何重写equals方法和hashCode方法

    在实际应用中经常会比较两个对象是否相等,比如下面的Address类,它有两个属性:String province 和 String city. public class Address {privat ...

  4. vue 调用webservice_动态调用WebService接口的几种方式

    一.什么是WebService? 这里就不再赘述了,想要了解的====>传送门 二.为什么要动态调用WebService接口? 一般在C#开发中调用webService服务中的接口都是通过引用过 ...

  5. C#.Net工作笔记010---c#中的静态扩展方法_可动态给string等_添加共通方法好用

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 之前,给list添加排序的扩展方法的时候用过.下面的作用是去掉list中重复的数据. /// &l ...

  6. java多态 重写(override)的调用优先级

    常规的说法是这样的: 在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有再到父类中查看,看是否从父类继承来.否则就要对参数进行转型,转成父类之后看是否有对应的方法.总的来说,方法调用的优先级 ...

  7. android jni 调用 java_Android与JNI(二) ---- Java调用C++ 动态调用

    目录: 1. 简介 2. JNI 组件的入口函数 3. 使用 registerNativeMethods 方法 4. 测试 5. JNI 帮助方法 6. 参考资料 1. 简介 Android与JNI( ...

  8. struts配置通配符*来匹配方法,实现动态调用

    01:web.xml中配置,启动struts2 <?xml version="1.0" encoding="UTF-8"?> <web-app ...

  9. vba传值调用_vba – 动态调用从形状OnAction属性传递参数的宏

    您可以为OnAction分配一个字符串,该字符串包含要调用的子句及其参数(请注意整个字符串用单引号括起来) 喜欢: Shape.OnAction = "'SubToCallOnAction ...

最新文章

  1. Request_获取请求参数通用方式介绍
  2. arcgis怎么做poi_跟着闫磊大神学ArcGIS,事半功倍
  3. 六、张正友标定法小结
  4. 网络基础:网络IP 、子网掩码 、路由器 、DNS知识分享
  5. 【壹刊】Azure AD(二)调用受Microsoft 标识平台保护的 ASP.NET Core Web API (上)
  6. 外包以小时计算金额的费用_2020年初级会计各大税种的计算公式,请收藏!
  7. arcore之路-unity开发从入门到实践_Unity游戏开发——单例模式的最佳实践
  8. web.config中httpRunTime的属性
  9. 销售面销五个最基础步骤步骤_3个步骤,可提供强有力的反馈
  10. [Java] 蓝桥杯ALGO-118 算法训练 连续正整数的和
  11. Java并发编程实战读书笔记5 ---Executor在android中的应用
  12. 线性同余法[纯理论]
  13. ERP实施项目的计划阶段要点分析
  14. excel小技巧之如何提取指定字符之前的字段
  15. linux密码weak,linux中__weak关键字
  16. 计算机发展趋势 网络化,计算机的发展趋势表现在多极化网络化等几个方面
  17. 系统繁忙 请稍后再试(ALI64)”
  18. el-popover的使用
  19. Ta-lib学习笔记01--成交量指标
  20. Windows10 在 Win+R启用以管理员身份运行的选项

热门文章

  1. jzoj5814 【NOIP提高A组模拟2018.8.14】 树 (树上期望,递归法列方程)
  2. 三星s9 测试性能软件,三星S9+和iPhone8+软件打开速度测试:谁才是状元郎
  3. Enfocus PitStop Pro 2019(PDF增强插件)
  4. 计算机1kb的二进制位数是,递接物品,错误的是()
  5. 浅谈工作型PPT之提高篇
  6. 虚拟服务器前置,ep虚拟主机接口干嘛用的(idc虚拟主机接口)
  7. 11 SMO优化算法
  8. 关于计算机听课如何做笔记,如何听课做笔记
  9. 手机上看杂志 谁借道谁沾光?
  10. 【区块链】Python开发EOS机器人与WAX链游脚本常用工具