`
boendev
  • 浏览: 242677 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

多态在 Java 和 C++ 编程语言中的实现比较(转载)

 
阅读更多

本文引用自:http://www.ibm.com/developerworks/cn/java/j-lo-polymorph/

众所周知,多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。C++ 和 Java 作为当前最为流行的两种面向对象编程语言,其内部对于多态的支持到底是如何实现的呢,本文对此做了全面的介绍。

注意到在本文中,指针和引用会互换使用,它们仅是一个抽象概念,表示和另一个对象的连接关系,无须在意其具体的实现。

Java 的实现方式

Java 对于方法调用动态绑定的实现主要依赖于方法表,但通过类引用调用和接口引用调用的实现则有所不同。总体而言,当某个方法被调用时,JVM 首先要查找相应的常量池,得到方法的符号引用,并查找调用类的方法表以确定该方法的直接引用,最后才真正调用该方法。以下分别对该过程中涉及到的相关部分做详细介绍。

JVM 的结构

典型的 Java 虚拟机的运行时结构如下图所示


图 1.JVM 运行时结构
图 1.JVM 运行时结构 

此结构中,我们只探讨和本文密切相关的方法区 (method area)。当程序运行需要某个类的定义时,载入子系统 (class loader subsystem) 装入所需的 class 文件,并在内部建立该类的类型信息,这个类型信息就存贮在方法区。类型信息一般包括该类的方法代码、类变量、成员变量的定义等等。可以说,类型信息就是类的 Java 文件在运行时的内部结构,包含了改类的所有在 Java 文件中定义的信息。

注意到,该类型信息和 class 对象是不同的。class 对象是 JVM 在载入某个类后于堆 (heap) 中创建的代表该类的对象,可以通过该 class 对象访问到该类型信息。比如最典型的应用,在 Java 反射中应用 class 对象访问到该类支持的所有方法,定义的成员变量等等。可以想象,JVM 在类型信息和 class 对象中维护着它们彼此的引用以便互相访问。两者的关系可以类比于进程对象与真正的进程之间的关系。

Java 的方法调用方式

Java 的方法调用有两类,动态方法调用与静态方法调用。静态方法调用是指对于类的静态方法的调用方式,是静态绑定的;而动态方法调用需要有方法调用所作用的对象,是动态绑定的。类调用 (invokestatic) 是在编译时刻就已经确定好具体调用方法的情况,而实例调用 (invokevirtual) 则是在调用的时候才确定具体的调用方法,这就是动态绑定,也是多态要解决的核心问题。

JVM 的方法调用指令有四个,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前两个是静态绑定,后两个是动态绑定的。本文也可以说是对于 JVM 后两种调用实现的考察。

常量池(constant pool)

常量池中保存的是一个 Java 类引用的一些常量信息,包含一些字符串常量及对于类的符号引用信息等。Java 代码编译生成的类文件中的常量池是静态常量池,当类被载入到虚拟机内部的时候,在内存中产生类的常量池叫运行时常量池。

常量池在逻辑上可以分成多个表,每个表包含一类的常量信息,本文只探讨对于 Java 调用相关的常量池表。

CONSTANT_Utf8_info

字符串常量表,该表包含该类所使用的所有字符串常量,比如代码中的字符串引用、引用的类名、方法的名字、其他引用的类与方法的字符串描述等等。其余常量池表中所涉及到的任何常量字符串都被索引至该表。

CONSTANT_Class_info

类信息表,包含任何被引用的类或接口的符号引用,每一个条目主要包含一个索引,指向 CONSTANT_Utf8_info 表,表示该类或接口的全限定名。

CONSTANT_NameAndType_info

名字类型表,包含引用的任意方法或字段的名称和描述符信息在字符串常量表中的索引。

CONSTANT_InterfaceMethodref_info

接口方法引用表,包含引用的任何接口方法的描述信息,主要包括类信息索引和名字类型索引。

CONSTANT_Methodref_info

类方法引用表,包含引用的任何类型方法的描述信息,主要包括类信息索引和名字类型索引。


图 2. 常量池各表的关系
图 2. 常量池各表的关系 

可以看到,给定任意一个方法的索引,在常量池中找到对应的条目后,可以得到该方法的类索引(class_index)和名字类型索引 (name_and_type_index), 进而得到该方法所属的类型信息和名称及描述符信息(参数,返回值等)。注意到所有的常量字符串都是存储在 CONSTANT_Utf8_info 中供其他表索引的。

方法表与方法调用

方法表是动态调用的核心,也是 Java 实现动态调用的主要方式。它被存储于方法区中的类型信息,包含有该类型所定义的所有方法及指向这些方法代码的指针,注意这些具体的方法代码可能是被覆写的方法,也可能是继承自基类的方法。

如有类定义 Person, Girl, Boy,


清单 1
				
 class Person { 
 public String toString(){ 
    return "I'm a person."; 
	 } 
 public void eat(){} 
 public void speak(){} 
	
 } 

 class Boy extends Person{ 
 public String toString(){ 
    return "I'm a boy"; 
	 } 
 public void speak(){} 
 public void fight(){} 
 } 

 class Girl extends Person{ 
 public String toString(){ 
    return "I'm a girl"; 
	 } 
 public void speak(){} 
 public void sing(){} 
 } 

当这三个类被载入到 Java 虚拟机之后,方法区中就包含了各自的类的信息。Girl 和 Boy 在方法区中的方法表可表示如下:


图 3.Boy 和 Girl 的方法表
图 3.Boy 和 Girl 的方法表 

可以看到,Girl 和 Boy 的方法表包含继承自 Object 的方法,继承自直接父类 Person 的方法及各自新定义的方法。注意方法表条目指向的具体的方法地址,如 Girl 的继承自 Object 的方法中,只有 toString() 指向自己的实现(Girl 的方法代码),其余皆指向 Object 的方法代码;其继承自于 Person 的方法 eat() 和 speak() 分别指向 Person 的方法实现和本身的实现。

Person 或 Object 的任意一个方法,在它们的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。

如调用如下:


清单 2
				
 class Party{ 
…
 void happyHour(){ 
 Person girl = new Girl(); 
 girl.speak(); 
…
	 } 
 } 

当编译 Party 类的时候,生成 girl.speak()的方法调用假设为:

Invokevirtual #12

设该调用代码对应着 girl.speak(); #12 是 Party 类的常量池的索引。JVM 执行该调用指令的过程如下所示:


图 4. 解析调用过程
图 4. 解析调用过程 

JVM 首先查看 Party 的常量池索引为 12 的条目(应为 CONSTANT_Methodref_info 类型,可视为方法调用的符号引用),进一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要调用的方法是 Person 的 speak 方法(注意引用 girl 是其基类 Person 类型),查看 Person 的方法表,得出 speak 方法在该方法表中的偏移量 15(offset),这就是该方法调用的直接引用。

当解析出方法调用的直接引用后(方法表偏移量 15),JVM 执行真正的方法调用:根据实例方法调用的参数 this 得到具体的对象(即 girl 所指向的位于堆中的对象),据此得到该对象对应的方法表 (Girl 的方法表 ),进而调用方法表中的某个偏移量所指向的方法(Girl 的 speak() 方法的实现)。

接口调用

因为 Java 类是可以同时实现多个接口的,而当用接口引用调用某个方法的时候,情况就有所不同了。Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同样的方法在基类和派生类的方法表的位置就可能不一样了。


清单 3
				
interface IDance{ 
   void dance(); 
 } 

 class Person { 
 public String toString(){ 
   return "I'm a person."; 
	 } 
 public void eat(){} 
 public void speak(){} 
	
 } 

 class Dancer extends Person 
 implements IDance { 
 public String toString(){ 
   return "I'm a dancer."; 
	 } 
 public void dance(){} 
 } 

 class Snake implements IDance{ 
 public String toString(){ 
   return "A snake."; 
	 } 
 public void dance(){ 
 //snake dance 
	 } 
 } 


图 5.Dancer 的方法表(查看大图
图 5.Dancer 的方法表 

可以看到,由于接口的介入,继承自于接口 IDance 的方法 dance()在类 Dancer 和 Snake 的方法表中的位置已经不一样了,显然我们无法通过给出方法表的偏移量来正确调用 Dancer 和 Snake 的这个方法。这也是 Java 中调用接口方法有其专有的调用指令(invokeinterface)的原因。

Java 对于接口方法的调用是采用搜索方法表的方式,对如下的方法调用

invokeinterface #13

JVM 首先查看常量池,确定方法调用的符号引用(名称、返回值等等),然后利用 this 指向的实例得到该实例的方法表,进而搜索方法表来找到合适的方法地址。

因为每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的。

C++ 的实现方式

从上文可以看到,Java 对于多态的实现依赖于方法表,但比较特殊的是,对于接口的支持是非常不同的,每次调用都要搜索方法表。实际上,在 C++ 中,单继承时对于多态的实现非常类似于 Java,但由于支持多重继承,这会碰到和 Java 支持接口动态调用同样的问题,C++ 的解决方案是利用对象的多个方法表指针,不幸的是,这会引入额外的指针调整的复杂性。

单继承

单继承时,C++ 对于多态的实现本质上与 Java 是一样的,也是基于方法表。但 C++ 在编译时就可以确认要调用的方法在方法表中的位置,而没有 JVM 在方法调用时查询常量池的过程。

C++ 编译时,编译器会自动做很多工作,其中之一就是在需要时在对象插入一个变量 vptr 指向类的方法表。如 Person,、Girl 的类定义与上文中 Java 类似,若


清单 4
				
class Person{ 
	 . . . 
 public : 
    Person (){} 
    virtual ~Person (){}; 
    virtual void speak (){}; 
    virtual void eat (){}; 
 }; 

class Girl : public Person{ 
	 . . . 
   public : 
   Girl(){} 
   virtual ~Girl(){}; 
   virtual void speak(){}; 
   virtual void sing(){}; 
 }; 

则 Person 与 Girl 实例的内存对象模型为:


图 6.Person 与 Girl 的对象模型
图 6.Person 与 Girl 的对象模型 

如下的调用代码

 Person *p = new Girl(); 
 p->speak(); 
 p->eat(); 

经编译器编译后调用代码为:

 p->vptr[1](p); 
 p->vptr[2](p); 

这样在运行时,会自然的过渡到对 Girl 的相应函数的调用。

可以看到方法表中没有各自的构造函数,这是因为 C++ 的方法表中仅含有用 virtual 修饰的方法,非 virtual 的方法是静态绑定的,没有必要占用方法表的空间。这与 Java 是不同的,Java 的方法表含有类所支持的所有的方法,可以说,Java 类的所有方法都是”virtual”(动态绑定)的。

多重继承

多重继承下,情况就完全不一样了,因为两个不同的类,其继承自与同一个基类的方法,在各自的方法表中的位置可能不同(和 Java 中的接口情况类似),但 Java 在运行时有 JVM 的支持,C++ 在这里引入了多个指向方法表的指针来解决这个问题,由此带来了调整指针位置的额外复杂性。

若有如下关系的三个类,Engineer 继承自 Person 和 Employee


图 7. 类静态结构关系图
图 7. 类静态结构关系图 

Engineer 实例对象模型为:


图 8.Engineer 对象模型
图 8.Engineer 对象模型 

可以看到 Engineer 实例有两个指向方法表的指针,这是与 Java 大不相同的。

设有如下的代码 ,


清单 5
				
 Engineer *p = new Engineer(); 
 Person * p1 = (Person *)p; 
 Empolyee *p2 = (Employee *)p; 

则各指针在运行时分别指向各自的子对象,如下所示:


图 7.Engineer 实例
图 7.Engineer 实例 

C++ 中对象的指针总是指向对象的起始处,如上述代码中,p 是 Engineer 对象的起始地址,而 p1 指向 p 转型成 Person 子对象的指针,可以看到实际上,两者是相等的;但 Employee 子对象的指针 p2 则于 p 和 p1 不同,实际上

 p2 = p + sizeof(Person); 
 p1->eat(); 
 p2->work(); 

则编译后生成的调用代码为:

 *(p1->vptr1[i]) (p1) 
 *(p2->vptr2[j]) (p2) 

某些情况下,甚至需要将 this 指针调整到整个对象的起始处,如:

 delete p2; 

析构函数的 this 指针要被调整到 p 所指向的位置,否则则会出现内存泄漏。设析构函数在方法表中的位置为 0,则编译后为:

 *(p2->vptr2[0]) (p) 

对于指针的调整,编译器没有足够的知识在编译时刻完成这个任务。如上例中,对于 p2 所指向的对象,该对象类型可能是 Employee 或任何该类的子类 ( 其它的子类如 Teacher 等 ),编译器无法确切的知道 p2 和整个对象的初始地址的距离 (offset), 这样的调整只能发生在运行时刻。

一般有两种方法来调整指针,如下图:


图 8. 指针调整 - 扩展方法表
图 8. 指针调整 - 扩展方法表 

这种方法将指针所有调整的 offset 存储于方法表的每个条目中,当调用方法表中的方法时,首先利用 offset 的值完成指针调整再做实际的调用。缺点显而易见,增加了方法表的大小,而且并不是每个方法都需要做指针调整。


图 9. 指针调整 -thunk 技术
图 9. 指针调整 -thunk 技术 

这就是所谓的 thunk 技术,方法表的每个条目指向一小段汇编代码,这段代码来保证做指针调整和调用正确的方法,相当于加了一层抽象。

多态在 Java 和 C++ 中的实现比较

上文分别对于多态在 Java 和 C++ 中的实现做了比较详细的介绍,下面对这两种语言的多态实现的异同做个小结:

  • 单继承情况下,两者实现在本质上相同,都是使用方法表,通过方法表的偏移量来调用具体的方法。
  • Java 的方法表中包含 Java 类所定义的所有实例方法,而 C++ 的方法表则只包含需要动态绑定的方法 (virtual 修饰的方法 )。这样,在 Java 下所有的实例方法都要通过方法表调用,而 C++ 中的非虚方法则是静态绑定的。
  • 任意 Java 对象只 “指向”一个方法表,而 C++ 在多重继承下则可能指向多个方法表,编译器保证这多个方法表的正确初始化。
  • 多层继承中 C++ 面临的主要问题是 this 指针的调整,设计更精巧更复杂;而 Java 在接口调用时完全采用搜索的方式,实现更直观,但调用效率比实例方法调用要慢许多。

可以看到,两者之间既有相似之处,也有不同的地方。对于单继承的实现本质上是一样的,但也有细微的差别(如方法表);差别最大的是对于多重继承(多重接口)的支持。实际上,由于 C++ 是静态编译型语言,它无法像 Java 那样,在运行时刻动态的“查找”所要调用的方法。


参考资料

学习

  • Java 虚拟机规范:Java 虚拟机规范规定了 Java 的具体工作方式,对 Java 语言的各个方面做了全面的阐述。 

  • Java 虚拟机专题:Java 虚拟机(Java virtual machine,JVM)是语言与底层软件和硬件之间的一种转换器。Java 语言的所有实现都必须实现 JVM,从而使 Java 程序可以在有 JVM 的任何系统上运行。 

  • 深入 Java 虚拟机:对 Java 虚拟机的各种可能实现做了独到而清晰的解析。 

  • 深入 C++ 对象模型:深入探讨了 C++ 的对象模型,函数调用机制等,对编译器在幕后所做的工作给出了详尽的解释。 

  • 技巧:用 C 语言实现程序的多态性:使用 C 语言模拟简单的多态特性,该文给出了一个简单的实现。 

  • developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。 

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。
分享到:
评论

相关推荐

    C++和Java多态的区别

    本文将对比分析C++和Java中多态特性的实现机制及其差异。 #### 二、C++中的多态实现 在C++中,多态主要通过虚函数来实现。当一个类声明了一个虚函数后,它的派生类可以重写该虚函数。通过基类指针调用派生类对象的...

    面向对象编程C++和Java比较教程

    面向对象编程(Object-...总之,C++和Java虽然都是面向对象的语言,但在实现细节和应用领域上有所区别。理解和掌握这两种语言的不同特性,将有助于程序员根据项目需求选择合适的工具,并在不同语言之间无缝切换。

    面向对象编程C++和Java比较教程 中英文完整版 pdf

    面向对象编程(Object-Oriented Programming,简称OOP)是一种重要的编程范式,它通过将数据和操作数据的方法封装在对象中,实现了程序设计的模块化和抽象化。本教程对比了两种广泛应用的面向对象语言——C++和Java...

    Java 语言程序设计:第5章接口多态.ppt

    多态是 Java 语言中的一种编程机制,它允许对象在运行时决定其行为。多态有两种形式:编译时多态和运行时多态。编译时多态是指在编译时确定对象的类型,而运行时多态是指在运行时确定对象的类型。 5.4 多态的应用 ...

    OOP语言技术比较:Java,C++,Object Pascal[1]

    标题和描述均提到了“OOP语言技术比较:Java,C++,Object Pascal”,这表明文章将对这三种面向对象编程(OOP)语言进行深入的技术对比分析。OOP是现代软件开发的核心概念之一,它通过封装、继承、多态等特性提供了...

    Java语言与C++语言的对比分析.pdf

    在计算机编程语言的发展历程中,Java和C++作为两个重要的语言,它们各自有着鲜明的特征和应用领域。Java语言由Sun Microsystems公司于1995年推出,而C++则源于1983年推出的C语言,由Bjarne Stroustrup开发。两种语言...

    c-and-cpp-language-learning, C和C++编程语言学习 - 2015级.zip

    C和C++编程语言是计算机科学中的基础且重要的部分,尤其对于系统级编程和高性能计算领域,它们的应用广泛且深入。这份"2015级"的学习资源压缩包旨在为初学者提供一个全面的C和C++学习路径,同时也适合有一定经验的...

    作业1:Java、C++和Python的OO特性调研报告1

    总结来说,Java、C++和 Python 在面向对象编程上有许多共同点,如类和对象的概念、封装、继承和多态。然而,它们在实现这些概念时各有特点,反映了不同的设计哲学和使用场景。Java 和 C++倾向于严谨的类型检查和访问...

    Java,C#,C++在继承,覆盖和多态,抽象类等几个方面的比较归纳

    Java、C#和C++是三种广泛使用的编程语言,它们在面向对象编程中都有相似但又各有特色的特性。本文将深入探讨它们在继承、覆盖、多态和抽象类这几个关键概念上的异同。 首先,让我们从继承说起。继承是面向对象编程...

    基于计算机软件安全开发的JAVA编程语言研究.pdf

    本文主要探讨了在计算机软件安全开发过程中,如何有效地融入JAVA编程语言的技术和方法。由于信息技术的飞速发展,计算机软件在人们的生活中扮演着越来越重要的角色,而软件的安全性成为了软件开发中极为重要的一环。...

    基于Python实现的一个高级代码补全工具,高质量为 Python、Java 和 C++ 等编程语言补全代码

    本文将详细介绍一个基于Python实现的高级代码补全工具,该工具专注于为Python、Java和C++等编程语言提供高质量的代码补全功能。 首先,我们要理解什么是代码补全。代码补全,也被称为自动完成或智能提示,是一种...

    java/c++区别

    此文档旨在全面对比Java和C++,整合了网络上的众多资源,旨在帮助读者理解和解决实际编程中遇到的问题。尽管它可能不如专门的书籍详细,但仍提供了丰富的对比信息。 (二) 个人学习感受 作者从自身经验出发,指出...

    C++代码转java工具

    C++和Java是两种不同的编程语言,每种都有其独特的语法和特性。C++是一种静态类型、编译式的、通用的、大小写敏感的、不仅支持过程化编程,也支持面向对象编程的语言。而Java是一种面向对象的、跨平台的、动态类型的...

    CPlus_to_Java_Converter;C++转java工具

    C++和Java是两种广泛应用且具有各自特点的编程语言。C++以其高效、灵活和底层控制闻名,而Java则以平台独立性、自动内存管理及丰富的类库受到青睐。本文将围绕“CPlus_to_Java_Converter”这一工具,探讨如何将C++...

    JAVA和C++实现面向对象方法的分析.pdf

    Java和C++都是支持面向对象编程的语言,它们各自在实现面向对象的方法上具有独特之处。本文将深入分析Java和C++在实现面向对象特性方面的差异,以及这些差异对程序设计水平的影响。 首先,面向对象编程的核心特性之...

    java与C++严格的比较.pdf

    对于希望从一种语言转向另一种语言的开发者而言,《Java与C++严格的比较》这篇文章提供了一个良好的起点,帮助他们理解和适应新语言的特点。 总之,在选择编程语言时,开发者应该考虑项目需求、团队经验以及个人...

    OOP语言技术比较:Java,C++,Object Pascal

    在程序设计模型方面,Java是一种纯OOP语言,所有代码都必须在类中,不允许全局变量和非面向对象的编程。C++和Object Pascal则是混合型,允许使用传统函数和过程,以及全局变量,提供更大的灵活性,但也可能导致代码...

    C/C++程序员java编程

    在实际编程中,本书还可能包含关于Java开发工具(如Eclipse或IntelliJ IDEA)、版本控制(如Git)、构建工具(如Maven或Gradle)以及测试框架(如JUnit)的使用介绍,这些都是现代Java开发的重要组成部分。...

Global site tag (gtag.js) - Google Analytics