多态性是面向对象程序设计代码重用的一个重要机制我们曾不只一次的提到Java多态性在Java运行时多态性继承和接口的实现一文中我们曾详细介绍了Java实现运行时多态性的动态方法调度今天我们再次深入Java核心一起学习Java中多态性的实现
polymorphism(多态)一词来自希腊语意为多种形式多数Java程序员把多态看作对象的一种能力使其能调用正确的方法版本尽管如此这种面向实现的观点导致了多态的神奇功能胜于仅仅把多态看成纯粹的概念
Java中的多态总是子类型的多态几乎是机械式产生了一些多态的行为使我们不去考虑其中涉及的类型问题本文研究了一种面向类型的对象观点分析了如何将对象能够 表现的行为和对象即将表现的行为分离开来抛开Java中的多态都是来自继承的概念我们仍然可以感到Java中的接口是一组没有公共代码的对象共享实 现
多态的分类
多态在面向对象语言中是个很普遍的概念虽然我们经常把多态混为一谈但实际上 有四种不同类型的多态在开始正式的子类型多态的细节讨论前然我们先来看看普通面向对象中的多态
Luca Cardelli和Peter Wegner(On Understanding Types Data Abstraction and Polymorphism一文的作者 文章参考资源链接)把多态分为两大类特定的和通用的四小类强制的重载的参数的和包含的他们的结构如下
)thisstylewidth=; border=>
在这样一个体系中多态表现出多种形式的能力通用多态引用有 相同结构类型的大量对象他们有着共同的特征特定的多态涉及的是小部分没有相同特征的对象四种多态可做以下描述
◆强制的一种隐 式做类型转换的方法
◆重载的将一个标志符用作多个意义
◆参数的为不同类型的参数提供相同的操作
◆包含的类包含关系的抽象操作
我将在讲述子类型多态前简单介绍一下这几种多态
强制的多态
强制多态隐式的将参数按某种方法转换成编译器认为正确的类型以避免错误在以下的表达式中编译器必须决定二元运算符+所应做的工作
+
+
+
第一个表达式将两个double的 操作数相加Java中特别声明了这种用法
第二个表达式将double型和int相加Java中没有明确定义这种运算不过编 译器隐式的将第二个操作数转换为double型并作double型的加法做对程序员来说十分方便否则将会抛出一个编译错误或者强制程序员显式的将 int转换为double
第三个表达式将double与一个String相加Java中同样没有定义这样的操作所以编译器将 double转换成String类型并将他们做串联
强制多态也会发生在方法调用中假设类Derived继承了类Base类C 有一个方法原型为m(Base)在下面的代码中编译器隐式的将Derived类的对象derived转化为Base类的对象这种隐式的转换使 m(Base)方法使用所有能转换成Base类的所有参数
C c = new C(); Derived derived = new Derived(); cm( derived );
并且隐式的强制转换可以避免 类型转换的麻烦减少编译错误当然编译器仍然会优先验证符合定义的对象类型
重载的多态
重载 允许用相同的运算符或方法去表示截然不同的意义+在上面的程序中有两个意思两个double型的数相加两个串相连另外还有整型相加长整 型等等这些运算符的重载依赖于编译器根据上下文做出的选择以往的编译器会把操作数隐式转换为完全符合操作符的类型虽然Java明确支持重载但 不支持用户定义的操作符重载
Java支持用户定义的函数重载一个类中可以有相同名字的方法这些方法可以有不同的意义这些重载 的方法中必须满足参数数目不同相同位置上的参数类型不同这些不同可以帮助编译器区分不同版本的方法
编译器以这种唯一表示的特 征来表示不同的方法比用名字表示更为有效据此所有的多态行为都能编译通过
强制和重载的多态都被分类为特定的多态因为这些多 态都是在特定的意义上的这些被划入多态的特性给程序员带来了很大的方便强制多态排除了麻烦的类型和编译错误重载多态像一块糖允许程序员用相同的名 字表示不同的方法很方便
参数的多态
参数多态允许把许多类型抽象成单一的表示例如List 抽象类中描述了一组具有同样特征的对象提供了一个通用的模板你可以通过指定一种类型以重用这个抽象类这些参数可以是任何用户定义的类型大量的用 户可以使用这个抽象类因此参数多态毫无疑问的成为最强大的多态
乍一看上面抽象类好像是javautilList的功能然 而Java实际上并不支持真正的安全类型风格的参数多态这也是javautilList和javautil的其他集合类是用原始的 javalangObject写的原因(参考我的文章A Primordial Interface? 以获得更多细节)Java的单根继承方式解决了部分问题但没有发挥出参数多态的全部功能Eric Allen有一篇精彩的文章Behold the Power of Parametric Polymorphism描述了Java通用类型的需求并建议给Sun的Java规格需求#号文档Add Generic Types to the Java Programming Language(参考资源链接)
包含的多态
包含多态通过值的类型和集合的包含关系实现了多态的行为在包括Java在内的众多面向对象语言中包含关系是子类型的所以Java的包含多态是子 类型的多态
在早期Java开发者们所提及的多态就特指子类型的多态通过一种面向类型的观点我们可以看到子类型多态的强大功 能以下的文章中我们将仔细探讨这个问题为简明起见下文中的多态均指包含多态
面向类型观点
图的UML类图给出了类和类型的简单继承关系以便于解释多 态机制模型中包含种类型个类和一个接口虽然UML中称为类图我把它看成类型图如Thanks Type and Gentle Class 一文中所述每个类和接口都是一种用户定义的类型按独立实现的观点(如面向类型的观点)下图中的每个矩形代表一种类型从实现方法看四种类型运用了 类的结构一种运用了接口的结构
image onmousewheel=javascript:return big(this) height= alt=图示范代码的UML类图 src=http://imgeducitycn/img_///gif width= onload=javascript:if(thiswidth>)thisstylewidth=; border=>
图示范代码的UML类图
以下的代码实现了每个用户 定义的数据类型我把实现写得很简单
用这样的类型声明和类的定义图从概念的观点描述了Java指令
Derived derived = new Derived();
image onmousewheel=javascript:return big(this) height= alt=图 Derived 对象上的引用 src=http://imgeducitycn/img_///gif width= onload=javascript:if(thiswidth>)thisstylewidth=; border=>
图 Derived 对象上的引用
上文中声明了 derived这个对象它是Derived类的图种的最顶层把Derived引用描述成一个集合的窗口虽然其下的Derived对象是可 见的这里为每个Derived类型的操作留了一个孔Derived对象的每个操作都去映射适当的代码按照上面的代码所描述的那样例 如Derived对象映射了在Derived中定义的m()方法而且还重载了Base类的m()方法一个Derived的引用变量无权访问 Base类中被重载的m()方法但这并不意味着不可以用superm()的方法调用去使用这个方法关系到derived这个引用的变量这个 代码是不合适的Derived的其他的操作映射同样表明了每种类型操作的代码执行
既然你有一个Derived对象可以用任 何一个Derived类型的变量去引用它如图所示Derived Base和IType都是Derived的基类所以Base类的引用是很有用的图描述了以下语句的概念观点
Base base = derived;
image onmousewheel=javascript:return big(this) height= alt=图Base类引用附于Derived对象之上 src=http://imgeducitycn/img_///gif width= onload=javascript:if(thiswidth>)thisstylewidth=; border=>
图Base类引用附于Derived对象之上
虽然Base类的引用不用再访问m()和m()但是却不会改变它Derived对象的任何特征及操作映射无论是变量derived还是 base其调用m()或m(String)所执行的代码都是一样的
两个引用之所以调用同一个行为是因为Derived对象并不知道去调用哪个方法对 象只知道什么时候调用它随着继承实现的顺序去执行这样的顺序决定了Derived对象调用Derived里的m()方法并调用Derived 里的m(String)方法这种结果取决于对象本身的类型而不是引用的类型
尽管如此但不意味着你用derived和 base引用的效果是完全一样的如图所示Base的引用只能看到Base类型拥有的操作所以虽然Derived有对方法m()和m()的 映射但是变量base不能访问这些方法
运行期的Derived对象保持了接受m()和m()方法的能力类型的限制使 Base的引用不能在编译期调用这些方法编译期的类型检查像一套铠甲保证了运行期对象只能和正确的操作进行相互作用换句话说类型定义了对象间相互 作用的边界
多态的依附性
类型的一致性是多态的核心对象上的每一个引用静态的类型检查器都要确认这样的依附和其对象的层次是一致的当一个引用成功的依附于另一个不同的对象 时有趣的多态现象就产生了(严格的说对象类型是指类的定义)你也可以把几个不同的引用依附于同一个对象在开始更有趣的场景前我们先来看一下下 面的情况为什么不会产生多态
多个引用依附于一个对象
图和图描述的例子是把两个及两个以上的 引用依附于一个对象虽然Derived对象在被依附之后仍保持了变量的类型但是图中的Base类型的引用依附之后其功能减少了结论很明显 把一个基类的引用依附于派生类的对象之上会减少其能力
一个开发这怎么会选择减少对象能力的方案呢?这种选择是间接的假设有一个名 为ref的引用依附于一个包含如下方法的类的对象
用一个Derived的参数调用poly(Base)是符合参数类型检查的
方法调用把一个本地Base类型的变量依附在一个引入的对象上所以虽然这个方法只接 受Base类型的参数但Derived对象仍是允许的开发这就不必选择丢失功能的方案从人眼在通过Derived对象时所看到的情况Base 类型引用的依附导致了功能的丧失但从执行的观点看每一个传入poly(Base)的参数都认为是Base的对象执行机并不在乎有多个引用指向同一 个对象它只注重把指向另一个对象的引用传给方法这些对象的类型不一致并不是主要问题执行器只关心给运行时的对象找到适当的实现面向类型的观点展示 了多态的巨大能力
附于多个对象的引用
让我们来看一下发生在poly(Base)中的多态行 为下面的代码创建了三个对象并通过引用传给poly(Base):
poly(Base)的实现代码是调用传进来的参数的m()方法图和图展示了 把三个类的对象传给方法时面向类型的所使用的体系结构
image onmousewheel=javascript:return big(this) height= alt=图将Base引用指向Derived类以及Base对象 src=http://imgeducitycn/img_///gif width= onload=javascript:if(thiswidth>)thisstylewidth=; border=>
图将Base引用指向Derived类以及Base对象
请注意每个图中方法m()的映射图中m()调用了Derived类的代码上面代码中的注释标明了ploy(Base)调用 Derivedm()图中Derived对象调用的仍然是Derived类的m()方法最后图中Base对象调用的m()是Base 类中定义的代码
多态的魅力何在?再来看一下poly(Base)的代码它可以接受任何属于Base类范畴的参数然而当他收 到一个Derived的对象时它实际上却调用了Derived版本的方法当你根据Base类派生出其他类时如 DerivedDerivedpoly(Base)都可以接受这些参数并作出选择调用合适的方法多态允许你在完成poly(Base)后扩 展它的用途
这看起来当然很神奇基本的理解展示了多态的内部工作原理在面向类型的观点中底层的对象所实现的代码是非实质性的 重要的是类型检查器会在编译期间为每个引用选择合适的代码以实现其方法多态使开发者运用面向类型的观点不考虑实现的细节这样有助于把类型和实现分 离(实际用处是把接口和实现分离)
对象接口
多态依赖于类型和实现的分离多用来把接口和实现分离但下面的观点好像把Java的关键字 interface搞得很糊涂
更为重要的使开发者们怎样理解短语the interface to an object典型地根据上下文这个短语的意思是指一切对象类中所定义的方法至一切对象公开的方法这种倾向于以实现为中心的观点较之于面向类型 的观点来说使我们更加注重于对象在运行期的能力图中引用面板的对象表面被标志成Derived Object这个面板上列出了Derived对象的所有可用的方法但是要理解多态我们必须从实现这一层次上解放出来并注意面向类型的透视图中 被标为Base Reference的面板在这一层意思上引用变量的类型指明了一个对象的表面这只是一个表面不是接口在类型一致的原则下我们可以用面向类型 的观点为一个对象依附多个引用对interface to an object这个短语的理解没有确定的理解
在类型概念 中the interface to an object refers 引用了面向类型观点的最大可能如图的情形把一个基类的引用指向相同的对象缩小了这样的观点如图所示类型概念能使人获得把对象间的 相互作用同实现细节分离的要领相对于一个对象的接口面向类型的观点更鼓励人们去使用一个对象的引用引用类型规定了对象间的相互作用当你考虑一个对 象能做什么的时候只需搞明白他的类型而不需要去考虑他的实现细节
Java接口
以上所谈到的 多态行为用到了类的继承关系所建立起来的子类型关系Java接口同样支持用户定义的类型相对地Java的接口机制启动了建立在类型层次结构上的多态 行为假设一个名为ref的引用变量并使其指向一个包含一下方法的类对象
为了弄明白poly(IType)中的多态以下的代码从不同的类创建两个对象并分别把他们传给 poly(IType)
上面的代码类似于关于poly(Base)中的多态的讨论poly(IType)的实现代码是调 用每个对象的 本地版本的m()方法如同以前代码的注释表明了每次调用所返回的CString类型的结果图表明了两次调用poly(IType)的概念结构
image onmousewheel=javascript:return big(this) height= alt=图指向Derived和Separate对象的IType引用 src=http://imgeducitycn/img_///gif width= onload=javascript:if(thiswidth>)thisstylewidth=; border=>
图指向Derived和Separate对象的IType引用
方法poly(Base)和poly(IType)中所表现的多态行为的相似之处可以从透视图中直接看出来把我们在实现在一层上的理解再提高一 层就可以看到这两段代码的技巧基类的引用指向了作为参数传进的类并且按照类型的限制调用对象的方法引用既不知道也不关心执行哪一段代码编译期间 的子类型关系检查保证了通过的对象有能力在被调用的时候选择合适的实现代码
然而他们在实现层上有一个重要的差别在 poly(Base)的例子中(图和图)BaseDerivedDerived的类继承结构为子类型关系的建立提供了条件并决定了方法去 调用哪段代码在poly(IType)的例子中(如图)则是完全不同的动态发生的Derived和Separate不共享任何实现的层次但 是他们还是通过IType的引用展示了多态的行为
这样的多态行为使Java的接口的功能的重大意义显得很明显图中的UML类图 说明了Derived是Base和IType的子类型通过完全脱离实现细节的类型的定义方法Java实现了多类型继承并且不存在Java所禁止的多 继承所带来的烦人的问题完全脱离实现层次的类可以按照Java接口实现分组在图中接口IType和DerivedSeparate以及这类型的 其他子类型应该划为一组
按照这种完全不同于实现层次的分类方法Java的接口机制是多态变得很方便哪怕不存在任何共享的实现或 者复写的方法如图所示一个IType的引用用多态的方法访问到了Derived和Separate对象的m()方法
再次探讨对象的接口
注意图中的Derived和Separate对象的对m()的映射方法如前所述每一个对象的接 口都包含方法m()但却没有办法用这两个对象使方法m()表现出多态的行为每一个对象占有一个m()方法是不够的必须存在一个可以操作 m()方法的类型通过这个类型可以看到对象这些对象似乎是共享了m()方法但在没有共同基类的条件下多态是不可能的通过对象的接口来看多 态会把这个概念搞混
结论
从全文所述的面向对象多态所建立起来的子类型多态你可以清楚地认识到这种面向类型的观点如果你想理解子类型多态的思想就应该把注意力从实现的细节转移到类型的上类型把对象分成组并且管理着这些对象的接口类型的 继承层次结构决定了实现多态所需的类型关系
有趣的是实现的细节并不影响子类型多态的层次结构类型决定了对象调用什么方法而实 现则决定了对象怎么执行这个方法也就是说类型表明了责任而负责实施的则是具体的实现将实现和类型分离后我们好像看到了这两个部分在一起跳舞类型决定了他的舞伴和舞蹈的名字而实现则是舞蹈动作的设计师