购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.7 多态

在编程中涉及类型层次时,我们通常会将一个对象视为其基类的一个实例,而不是对象实际的类。这种方式可以让你在编写代码时不依赖于具体的类。在形状的例子中,方法都是作用于通用的形状,而不需要关心该形状具体是圆形、矩形、三角形,还是一个没有明确定义的形状。因为所有的形状都可以被绘制、清除、移动,所以当这些方法发送消息至对象的时候,就无须关注对象是如何处理这条消息的。

当我们添加新的类时,这些代码是不受影响的,添加新的类可以扩展面向对象程序的能力,从而能够处理一些新的情况。比如,你为基类“形状”创建了一个子类“五边形”,并且不改变那些基于通用形状的方法。这种通过派生子类就可以轻松扩展程序设计的能力,是封装变化的一种基础方式。这种方式在改善设计的同时,也降低了软件维护的成本。

当你尝试用派生的子类替代通用基类(比如,把圆形当作形状,把自行车当作交通工具,把鸬鹚当作鸟等)时会发现一个问题,即调用方法来绘制这个通用的形状、驾驶这辆通用的交通工具或者让这只鸟飞翔时,编译器并不知道在编译时具体需要执行哪一段代码。那么重点来了,当消息被发送时,程序员并不关心具体执行的是哪一段代码。也就是说,当负责绘制的方法应用于圆形、矩形或者三角形时,这些对象将能够根据其类型执行对应的正确代码。

如果你并不关心具体执行的是哪一段代码,那么当你添加新的子类时,即使不对其基类的代码做任何修改,该子类实际执行的代码可能也会有所不同。但如果编译器无法得知应该具体执行哪一段代码,它会怎么做呢?比如下图中的BirdController对象,它可以和通用的Bird对象协同工作,同时它并不知道这些对象具体是什么类型的鸟。对于BirdController来说,这种方式非常方便,因为它无须额外编写代码来确定这些对象的具体类型和行为。那么问题来了,当一个Bird对象的move()方法被调用时,如果我们并不清楚其具体的类型,该如何确保最终执行的是符合预期的正确行为呢(比如Goose对象执行的是行走、飞翔或游泳,Penguin对象则是移动或游泳,见图1-8)?

图1-8

答案来自继承机制的一种重要技巧:编译器并非通过传统方式来调用方法。对于非面向对象编译器而言,其生成的函数调用会触发“前期绑定”(early binding),这是一个你可能从来都没听说过的词,因为你从未考虑过使用这种方式。前期绑定意味着编译器会生成对一个具体方法名的调用,该方法名决定了被执行代码的绝对地址。但是对于继承而言,程序直到运行时才能明确代码的地址,所以就需要引入其他可行的方案以确保消息可以顺利发送至对象。

为了解决上面提及的问题,面向对象语言使用的机制是“后期绑定”(late binding)。也就是说,当你向某个对象发送消息时,直到运行时才会确定哪一段代码会被调用。编译器会确保被调用的方法是真实存在的,并对该方法的参数和返回值进行类型检查,但是它并不知道具体执行的是哪一段代码。

为了实现后期绑定,Java使用了一些极为特殊的代码以代替直接的函数调用,这段代码使用存储在对象中的信息来计算方法体的地址(第9章会详细地描述这个过程)。其结果就是,在这些特殊代码的作用下,每一个对象会有不同的表现。通俗地讲,当你向一个对象发送消息时,该对象自己会找到解决之道。

顺便一提,在某些编程语言里,你必须显式地为方法赋予这种后期绑定特性。比如,C++使用virtual关键字来达到此目的。在这些编程语言中,方法并不默认具备动态绑定特性。不过,Java默认具备动态绑定特性,所以你无须借助于其他关键字或代码来实现多态。

我们再来看一下形状的例子。之前的图中展示了一些形状的类(这些类都基于统一的接口),为了更好地描述多态,我们编写一小段只关注基类而不关注具体子类的代码。由于这段代码不关注类的细节,因此非常简单易懂。此外,如果我们通过继承添加了一个新的子类“六边形”,我们的代码仍然适用于这个新的Shape类,就像适用于其他已有子类一样。因此可以说,这段程序具备扩展性。

如果你用Java编写一个方法(你马上就会学到具体应该怎么做):

这个方法适用于任何Shape对象,所以它不关心进行绘制和清除的对象具体是什么类型。如果程序的其他地方调用了doSomething()方法,比如:

不管对象具体属于哪个类,doSomething()方法都可以正常运行。

简直妙不可言。我们再看这一行代码:

在这段代码里,原本我们需要传递一个Shape对象作为参数,而实际传递的参数却是一个Circle类的对象。因为Circle也是一个Shape,所以doSomething()也可以接受Circle。也就是说,doSomething()发送给Shape对象的任何消息也可以发送给Circle对象。这是一种非常安全且逻辑清晰的做法。

这种将子类视为基类的过程叫作“向上转型”(upcasting)。这里的“转型”指的是转变对象的类型,而“向上”沿用的是继承图的常规构图,即基类位于图的顶部,数个子类则扇形分布于下方。因此,转变为基类在继承图中的路径就是一路向上,也就叫作“向上转型”(见图1-9)。

图1-9

面向对象程序总会包含一些向上转型的代码,因为这样就可以让我们无须关心对象具体的类是什么。再看一下doSomething()方法中包含的代码:

需要注意的是,代码并没有告诉我们,“如果是一个Circle请这样做,如果是一个Square请那样做,诸如此类”。如果你真的编写了一段代码用于检查所有可能出现的形状,那么这段代码必然是一团糟,并且每当你为Shape添加一个新的子类时,都必须修改这段代码。所以,上面的代码实际上做的是:“这是一个Shape,我知道它可以进行绘制和清除,那就这么干吧,具体细节交给形状自己处理就好”。

doSomething()方法的神奇之处在于,代码运行的结果是符合预期的。如果直接通过Circle、Square或者Line对象调用draw()方法,运行的代码自然是不同的。如果调用draw()方法时并不知道Shape对象的具体类型,它也能正常工作,即执行其实际子类的代码。这一点十分了不起,因为当Java编译器编译doSomething()的代码时,它并不知道对象的类型是什么。通常来说,你可能会想当然地认为被调用的是基类Shape的erase()和draw()方法,而非具体的Circle、Square或者Line子类,然而实际情况是,确实是具体的子类被调用了,这就是多态。编译器和运行时系统负责处理各种细节,你需要了解的就是多态机制的存在,更重要的是要知道如何利用多态进行设计。当你向一个对象发送消息时,哪怕需要用到向上转型,该对象也能够正确地处理该消息。 38gPTyQpCOKVl8huYOEvpAJcWJBQ95wdNNzCpvql6BMUIJT/utzbQU4rZwvSwVwg

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开