virtual_table

第十二章 多态(Polymorphism)的实现与Virtual Table

1 简介

在面向对象程序设计中,多态特性是一项非常重要和强大的工具。在程序设计中,它为设计与实现的分离提供了有力的支持。具体地讲,当使用一个接口或者基类对象引用调用成员方法时,具体调用的方法是由这个引用指向的对象类型决定的。因为这个决定不是在编译时做出的,因此,多态又被称为动态绑定(Dynamic Binding)。使用多态技术有多种好处。当设计与实现分离后,任意一个实现的变化都不会影响接口和其他实现。这极大的降低了软件更新和维护的开销。那么,多态是如何实现的呢?换句话说,成员方法名是如何动态绑定到一个具体的方法实现上的呢?本章着重回答这个问题。

2 Virtual Table

使用Virtual Table是一种常见的实现多态的方法,Java和C++语言都是通过Virtual Table来实现成员方法的动态绑定的,但是,它们实现的细节是不同的。本文先从概念上介绍Virtual Table,然后在第三小节介绍Java如何将Virtual Table的概念应用于.class文件和Java虚拟机中,实现成员方法的动态绑定。

从概念上讲,Virtual Table是一个映射函数(Mapping Function),将(变量或者方法的)名字映射到一个实现对象上。这种映射又被称为绑定(Binding)。多态是一种从成员方法名字到成员方法实现的映射(绑定)。

更具体一点的讲,上述的映射函数是由一个表实现的。如图一所示,图中左侧的是一个动态绑定表。表中每个元素是一个二元组,由函数名字和函数引用组成。这个二元组则表示由函数名字到函数实现的映射,或者将函数名字绑定到一个具体的函数对象上。在这里,函数引用可以看作是C语言中的函数指针,或者Java语言中的Method Reference。如果这个映射表是在编译时确定下来的,那么,这个映射表被称为静态绑定表。如果这个映射表是运行时才能确定的,那么,这个映射表被称为动态绑定表。

图一 函数动态绑定表

图一 函数动态绑定表

在面向对象程序设计中,每个对象都隐含着上述的映射表。这个映射表是由编译器生成的。这个映射表被放在对象中的固定位置,所以,在成员方法调用时,每个对象都能找到相对应的函数映射表。如图二所示,一个Vehicle对象中包含了一个成员变量vTable;该变量指向对象Vehicle的成员方法映射表。然后,再由成员方法映射表查找对应的函数实现。

图二 对象与成员方法映射表

图二 对象与成员方法映射表

所以,在代码中,如果获得了一个Vehicle的对象v,并调用v.foo(),则编译器会将其转换为v.vtable[0]()。其中,v.vtable表示对象v内部的成员方法映射表;v.vtable[0]表示获取该映射表中第一个元素中的函数引用(即Vehicle::foo函数);v.vtable[0]()则是调用所获得的方法。

Vehicle v = new Vehicle();
v.foo(); // 编译器会转换为v.vtable[0]()

除此之外,编译器还会向类的构造函数添加一些代码,用以初始化上述的成员方法映射表。在编译过程中,类的继承关系已经确定下来了。所以,编译器能够获得成员方法映射的信息,并未每个新的对象设置好该映射表。在如下的代码中,父类Vehicle实现了两个成员方法foo()和bar()。子类Car覆盖了成员方法bar()。因此,在Java语言中,在对Car类的对象调用bar()方法时,实际上调用的是Car类中的bar()方法。

public class Vehicle {
    public void foo() {}
    public void bar() {}  // 此方法未被调用
}

public class Car {
    public void bar() {}  // 此方法被调用

    public static void main(String[] args) {
        Vehicle v = new Car();
        v.bar(); // 调用的是Car类中的bar() 方法。
    }
}

Vehicle类和Car类对象的内部结构如下。在编译时,编译器已经获知Car类会复用Vehicle类的foo()方法,所以,在Car的构造函数中,会将foo方法的引用设置为Vehicle类中的foo()方法,而将bar方法的引用设置为Car类中的bar()。

图三 Vehicle和Car对象的内部结构图

图三 Vehicle类和Car类对象的内部结构图

 

3 Java语言中Virtual Table的实现

在Java语言中,Virtual Table是存放在.class文件中的。Java编译器将Virtual Table写入.class文件,再由Java虚拟机将其加载入内存。我们会在后续章节详细介绍.class文件的内容。在这里,我们仅简单介绍与Virtual Table相关的内容。

Java编译器会为每个类生成一个.class文件。.class文件实际上是一个二进制的结构体;其中,除了包含魔数(magic number),主/次版本号以外,直接父类和成员方法数组是该结构体的两个重要组成部分。该成员方法数组描述的是这个类声明的或实现的所有成员方法,不包含在父类中声明的方法。父类的方法可以通过“直接父类”信息找到其直接父类的.class文件,从而查找到父类所声明的所有的成员方法。以此类推,直到查找到Object类。

struct ClassFile {
    u4 magic;  // 魔数
    u2 minor_version; // 次版本号
    u2 major_version; // 主版本号
    ...
    u2 super_class; // 直接父类
    ...
    u2 methods_count; // 成员方法个数
    method_info methods[methods_count]; // 成员方法数组
    ...
}

因此,与图三稍稍不同的是,在Java的实现中,Vehicle和Car类之间还保持着继承关系。如图四所示,左下角的是一个Car类的对象car。该对象包含一个成员变量class。从该成员变量中可以查找到相应的类信息(Car Class)。(注:这个过程实际上是通过调用Object类的getClass()成员方法实现的。这个调用过程也是一个动态绑定/查找的过程。)。在Car Class对象中,成员变量super_class指向基类Vehicle的Class对象。Car Class的成员变量methods保存了所有在Car类声明/定义的成员方法。

图四 Vehicle和Car对象的Java内部结构图

图四 Vehicle和Car对象的Java内部结构图

当调用成员方法时,应调用的方法的查找过程(即:动态绑定的查找过程)如下:

  1. 如果在该对象的类定义中查找到了所调用的成员方法,则该方法即为选中的调用方法。
  2. 如果在该对象的类定义中未找到,且该类是子类,则在其直接父类中查找。
  3. 重复上述过程,直到在某一父类中找到选中的调用方法,或者查找失败。

4 结语

本章介绍了面向对象程序设计中多态的一种实现方式。Virtual Table的概念被多种语言所采用,然而,它们的实现方式不尽相同。Java语言是通过在Java虚拟机中实时查找相应的成员方法而实现的动态绑定。虽然这种查找过程并不是最高效的实现方式,但是,它却完美的融入了Java反射机制,通过类之间的继承关系,逐级查找对应的成员方法。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.