上两篇篇博文讨论了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

代码例如以下:

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

注意,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方法。

字节码指令:

  1. public static void main(java.lang.String[]);
  2. Code:
  3. Stack=2, Locals=3, Args_size=1
  4. 0: new #2; //class ClassReference$Boy
  5. 3: dup
  6. 4: invokespecial #3; //Method ClassReference$Boy."<init>":()V
  7. 7: astore_1
  8. 8: new #4; //class ClassReference$Girl
  9. 11: dup
  10. 12: invokespecial #5; //Method ClassReference$Girl."<init>":()V
  11. 15: astore_2
  12. 16: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
  13. 19: aload_1
  14. 20: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  15. 23: aload_1
  16. 24: invokevirtual #8; //Method ClassReference$Person.eat:()V
  17. 27: aload_1
  18. 28: invokevirtual #9; //Method ClassReference$Person.speak:()V
  19. 31: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
  20. 34: aload_2
  21. 35: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  22. 38: aload_2
  23. 39: invokevirtual #8; //Method ClassReference$Person.eat:()V
  24. 42: aload_2
  25. 43: invokevirtual #9; //Method ClassReference$Person.speak:()V
  26. 46: return

当中全部的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

代码例如以下:

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

上面的代码中dancer.speak(); dancer.eat(); 这两句同样不能调用。

运行结果例如以下所看到的:



其字节码指令例如以下所看到的:

  1. public static void main(java.lang.String[]);
  2. Code:
  3. Stack=2, Locals=3, Args_size=1
  4. 0: new #2; //class InterfaceReference$Dancer
  5. 3: dup
  6. 4: invokespecial #3; //Method InterfaceReference$Dancer."<init>":()V
  7. 7: astore_1
  8. 8: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
  9. 11: aload_1
  10. 12: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  11. 15: aload_1
  12. 16: invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
  13. 21: new #7; //class InterfaceReference$Snake
  14. 24: dup
  15. 25: invokespecial #8; //Method InterfaceReference$Snake."<init>":()V
  16. 28: astore_2
  17. 29: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
  18. 32: aload_2
  19. 33: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  20. 36: aload_2
  21. 37: invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
  22. 42: return

从上面的字节码指令能够看到,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方法,会发生什么?

代码例如以下:

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

运行结果例如以下:



能够看到System.out.println(dancer); 调用的是Person的toString方法。

内存模型例如以下所看到的:

结束语

这篇博文讨论了invokevirtual和invokeinterface的内部实现的差别,以及override的实现原理。

下一步,打算讨论下invokevirtual的详细实现细节。如:怎样实现符号引用到直接引用的转换的?可能会看下OpenJDK底层的C++实现。

參考资料

java方法调用之动态调用多态(重写override)的实现原理——方法表(三)的更多相关文章

  1. 实习第一周第一天:接口 extends是继承类,implement是实现接口,原接口里面的方法填充,方法名也是不变,重写override是父类的方法名不变,把方法体给改了

    一.定义 Java接口(Interface),是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为( ...

  2. C++调用DLL有两种方法——静态调用和动态调用

    C++调用DLL有两种方法——静态调用和动态调用 标签: dllc++winapinullc 2011-09-09 09:49 11609人阅读 评论(0) 收藏 举报  分类: cpp(30)  [ ...

  3. c/c++ 继承与多态 静态调用与动态调用

    静态调用,与动态调用. 动态调用条件:必须用父类的引用或者指针,并且调用的必须是虚方法. #include <iostream> class Base{ public: virtual i ...

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

    本文“[VB技巧]VB静态调用与动态调用dll详解”,来自:Nuclear'Atk 网络安全研究中心,本文地址:http://lcx.cc/?i=489,转载请注明作者及出处! [[请注意]]:在以下 ...

  5. dll静态调用和动态调用

    动态链接库有2种连接方式,一种是通过库直接加入(又叫隐式加载或载入时加载),一种是在运行时加入.后者很好理解,比如LoadLibrary(),GetProcAddress()获取想要引入的函数,使用完 ...

  6. C# 调用WebService的3种方式 :直接调用、根据wsdl生成webservice的.cs文件及生成dll调用、动态调用

    1.直接调用 已知webservice路径,则可以直接 添加服务引用--高级--添加web引用 直接输入webservice URL.这个比较常见也很简单 即有完整的webservice文件目录如下图 ...

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

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

  8. 编写Java程序,在子类老虎中重写父类动物的吃食方法

    返回本章节 返回作业目录 需求说明: 在子类老虎中重写父类动物的吃食方法 实现思路: 在子类老虎中重写父类动物的吃食方法的实现思路如下: 创建各种动物的父类Animal类,在该类中定义eat()方法. ...

  9. DLL的静态调用和动态调用

    // ------------------------------------DLL源代码 circle.dproj -------------------------------------libr ...

随机推荐

  1. 【Linux下权限控制之chmod与chown命令】

    chmod 用于配置文件/目录权限 命名格式:chmod [选项] 文件/目录名 . 权限类别: r 读取 可用数字4表示 w 写入 可用数字2表示 x 执行 可用数字1表示 . 归属类别: u 属主 ...

  2. b模式处理文件

    1.读 f=open('cheng','rb') date=f.read() print(date.decode()) 2.写 f=open('cheng','ab') f.write('chengz ...

  3. 判断控件的CGRect是否重合,获取控件的最大XY值

    判断给定的点是否被一个CGRect包含: BOOL contains = CGRectContainsPoint(CGRect rect, CGPoint point); 判断一个CGRect是否和另 ...

  4. 【Uva 10618】Tango Tango Insurrection

    [Link]: [Description] 玩跳舞机. 有一定的约束. 归纳起来就是以下三点 1.两只脚不能同时踩一个位置 2.如果左脚踩在了右键上,那么下一次移动的一定要是左脚 3.如果右脚踩在了左 ...

  5. Codeforces 164 D Minimum Diameter

    题目链接~~> 做题感悟:越来越感觉CF的题非常好,非常有深度. 解题思路: 这题须要注意 k 的大小.由于 k 仅仅有 30 个,终于形成的点的直径一定是某个确定的值,所以我们能够枚举这个值. ...

  6. php 内置的 html 格式化/美化tidy函数 -- 让你的HTML更美观

    php 内置的 html 格式化/美化tidy函数 https://github.com/htacg/tidy-html5 # HTML 格式化 function beautify_html($htm ...

  7. es69

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. 通过Debug-->Attach to Process的方式来调试网站

    找到网站所对应的应用程序池

  9. 73,QT指针数组实战(指针数组与数组指针)

    //指针数组,每一个指针都是一个MainWindow // MainWindow *w[3][4]; // for(int i=0;i<3;i++) // { // for(int j=0;j& ...

  10. Impala性能优化

    不多说,直接上干货! • 执行计划 – 查询sql执行之前,先对该sql做一个分析,列出需要完成这一项查询的详细方案 – 命令:explain sql.profile 要点: • 1.SQL优化,使用 ...