全澳洲大学排名:1.7 伴随多态的可互换对象

来源:百度文库 编辑:中财网 时间:2024/04/30 03:58:50
1.7 伴随多态的可互换对象
在处理类型的层次结构时,经常想把一个对象不当作它所属的特定类型来对待,而是将其当作其基类的对象来对待。这使得人们可以编写出不依赖于特定类型的代码。在"几何形"的例子中,方法操作的都是泛化(generic)的形状,而不关心它们是圆形、正方形、三角形还是其他什么尚未定义的形状。所有的几何形状都可以被绘制、擦除和移动,所以这些方法都是直接对一个几何形对象发送消息;它们不用担心对象将如何处理消息。
这样的代码是不会受添加新类型影响的,而且添加新类型是扩展一个面向对象程序以便处理新情况的最常用方式。例如,可以从"几何形"中导出一个新的子类型"五角形",而并不需要修改处理泛化几何形状的方法。通过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一。这种能力可以极大地改善我们的设计,同时也降低软件维护的代价。
但是,在试图将导出类型的对象当作其泛化基类型对象来看待时(把圆形看作是几何形,把自行车看作是交通工具,把鸬鹚看作是鸟等等),仍然存在一个问题。如果某个方法要让泛化几何形状绘制自己、让泛化交通工具行驶,或者让泛化的鸟类移动,那么编译器在编译时是不可能知道应该执行哪一段代码的。这就是关键所在:当发送这样的消息时,程序员并不想知道哪一段代码将被执行;绘图方法可以被等同地应用于圆形、正方形、三角形,而对象会依据自身的具体类型来执行恰当的代码。
如果不需要知道哪一段代码会被执行,那么当添加新的子类型时,不需要更改调用它的方法,它就能够执行不同的代码。因此,编译器无法精确地了解哪一段代码将会被执行,那么它该怎么办呢?例如,在下面的图中,BirdController对象仅仅处理泛化的Bird对象,而不了解它们的确切类型。从BirdController的角度看,这么做非常方便,因为不需要编写特别的代码来判定要处理的Bird对象的确切类型或其行为。当move()方法被调用时,即便忽略Bird的具体类型,也会产生正确的行为(Goose(鹅)走、飞或游泳,Penguin(企鹅)走或游泳),那么,这是如何发生的呢? 这个问题的答案,也是面向对象程序设计的最重要的妙诀:编译器不可能产生传统意义上的函数调用。一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定,这个术语你可能以前从未听说过,可能从未想过函数调用的其他方式。这么做意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。然而在OOP中,程序直到运行时才能够确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他的机制。
为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查(无法提供此类保证的语言被称为是弱类型的),但是并不知道将被执行的确切代码。
为了执行后期绑定,Java使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址(这个过程将在第8章中详述)。这样,根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道对这条消息应该做些什么。
在某些语言中,必须明确地声明希望某个方法具备后期绑定属性所带来的灵活性(C++是使用virtual关键字来实现的)。在这些语言中,方法在默认情况下不是动态绑定的。而在Java中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。
再来看看几何形状的例子。整个类族(其中所有的类都基于相同的一致接口)在本章前面已有图示。为了说明多态,我们要编写一段代码,它忽略类型的具体细节,仅仅和基类交互。这段代码和具体类型信息是分离的(decoupled),这样做使代码编写更为简单,也更易于理解。而且,如果通过继承机制添加一个新类型,例如Hexagon(六边形),所编写的代码对Shape(几何形)的新类型的处理与对已有类型的处理会同样出色。正因为如此,可以称这个程序是可扩展的。
如果用Java来编写一个方法(后面很快你就会学习如何编写): 这个方法可以与任何Shape对话,因此它是独立于任何它要绘制和擦除的对象的具体类型的。如果程序中其他部分用到了doSomething()方法: 对doSomething()的调用会自动地正确处理,而不管对象的确切类型。
这是一个相当令人惊奇的诀窍。看看下面这行代码: 当Circle被传入到预期接收Shape的方法中,究竟会发生什么。由于Circle可以被doSomething()看作是Shape,也就是说,doSomething()可以发送给Shape的任何消息,Circle都可以接收,那么,这么做是完全安全且合乎逻辑的。
把将导出类看做是它的基类的过程称为向上转型(upcasting)。转型(cast)这个名称的灵感来自于模型铸造的塑模动作;而向上(up)这个词来源于继承图的典型布局方式:通常基类在顶部,而导出类在其下部散开。因此,转型为一个基类就是在继承图中向上移动,即“向上转型”(如下图所示)。 一个面向对象程序肯定会在某处包含向上转型,因为这正是将自己从必须知道确切类型中解放出来的关键。让我们再看看doSomething()中的代码: 注意这些代码并不是说“如果是Circle,请这样做;如果是Square,请那样做……”。如果编写了那种检查Shape所有实际可能类型的代码,那么这段代码肯定是杂乱不堪的,而且在每次添加了Shape的新类型之后都要去修改这段代码。这里所要表达的意思仅仅是“你是一个Shape,我知道你可以erase()和draw()你自己,那么去做吧,但是要注意细节的正确性。
” doSomething()的代码给人印象深刻之处在于,不知何故,它总是做了该做的。调用Circle的draw()方法所执行的代码与调用Square或Line的draw()方法所执行的代码是不同的,而且当draw()消息被发送给一个匿名的Shape时,也会基于该Shape的实际类型产生正确的行为。这相当神奇,因为就像在前面提到的,当Java编译器在编译doSomething()的代码时,并不能确切知道doSomething()要处理的确切类型。所以通常会期望它的编译结果是调用基类Shape的erase()和draw()版本,而不是具体的Circle、Square或Line的相应版本。正是因为多态才使得事情总是能够被正确处理。编译器和运行系统会处理相关的细节,你需要马上知道的只是事情会发生,更重要的是怎样通过它来设计。当向一个对象发送消息时,即使涉及向上转型,该对象也知道要执行什么样的正确行为。