1.7 伴随多态的可互换对象

来源:互联网 发布:用友软件 t系列 编辑:程序博客网 时间:2024/05/16 17:13

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