
1.10 多态性
多态性是一个希腊词,字面上理解为许多形状。尽管多态性与继承是紧耦合的关系,但它通常被单独引用为面向对象技术中最强大的优点之一。当向一个对象发送一条消息时,该对象必须定义一个方法来响应这条消息。在继承体系图中,所有的子类从它们的超类中继承接口。然而,由于每个子类是单独的实体,每个子类需要对同一条消息有单独的应答。
例如Shape类有名为draw的行为。当你告诉某人画一个形状时,你被问到的第一个问题是:“什么形状?”没人能绘制一个形状,因为形状是一个抽象概念(事实上Shape代码中的draw方法并不包含实现)。你必须指定一个具体的形状。比如你需要为Circle提供具体的实现。虽然Shape有一个draw方法,但Circle继承了该方法并提供了对该方法的实现。覆盖(overriding)的基本释义是子类替换父类中的一个实现。
例如,假设你有Circle、Square和Star三个形状组成的数组。尽管你把它们都当作Shape对象,并为每个Shape对象都发送一条draw消息,但最终结果是每个图形都是不一样的,因为Circle、Square和Star提供了各自的具体实现。总之,每个类能够对同一个draw方法返回不同的响应来绘制自己。这正是多态性的意义。
Shape类的代码如下所示:

Shape类有一个名为area的属性,它保存了形状的面积。getArea()方法具有一个abstract修饰符。如果方法被定义为abstract,子类必须提供该方法的实现。在本例中,Shape要求子类提供getArea()的实现。现在创建一个名为Circle的类并继承自Shape(extends关键字指定了Circle继承自Shape):


我们引入了一个名为构造函数(constructor)的新概念。Circle类有一个同名的方法Circle。如果方法名与类名相同,并且没有返回值,这个方法就是一个特殊的方法,称为构造函数。可以认为构造函数是类的入口,对象在这里被构造。构造函数是进行初始化操作和执行启动任务的好地方。
Circle构造函数接收单个参数。这个参数代表半径,并且赋值给Circle类的radius属性。
Circle类也提供了getArea方法的实现,而在Shape类中getArea定义为抽象方法。
我们创建了一个名为Rectangle的相似的类:

现在我们可以创建任意数量的矩形和圆,只需调用它们的getArea()方法即可。这是因为我们知道所有的矩形和圆都继承自Shape,所有的Shape类都有getArea()方法。如果子类继承了父类的一个抽象方法,它必须提供该方法的具体实现,否则它自身也必须是个抽象类(请见图1.18的UML图)。这种方式提供了快速创建其他新类的机制。

图1.18 Shape UML图
我们可以使用以下方式来实例化Shape类:

然后使用栈(stack)这样的结构,把这些Shape类放到栈中:

什么是栈(Stack)?
栈是一种数据结构,表示一个后进先出的系统。它像一个自动换币器,你把硬币插入到圆柱的最上头,当你需要硬币时,则从最上头拿,即拿的是最后一次放入的硬币。将一个东西推入栈意味着你把该东西加入到了最顶端(比如将一个硬币插入到自动换币器中)。从栈中取一个东西意味着你拿走了最近一次放入栈的东西(比如从最顶处取硬币)。
接下来是最好玩的部分。我们可以清空栈,而不用担心栈中存入了哪种Shape类(我只需知道它们都是形状):

实际上,我们对所有形状发送了相同的信息:

然而产生的实际行为取决于形状类型。例如,Circle计算的是圆的面积,而Rectangle计算的是矩形的面积。实际上,我们给Shape类发送了一条消息,而期望根据使用的Shape子类类型来返回不同的行为。这体现了多态性的关键概念。
这种方式就是为类之间(以及应用程序之间)提供标准化的接口。比如办公套件应用程序包含一个文字处理应用和一个电子表格应用。假设这两个都有一个名为Office的类,包含一个名为print()的接口。办公套件中的所有类都要实现print()接口。有意思的是尽管文字处理和电子表格都调用print()接口,但它们会做不同的事情,一个打印文字处理文档,另一个打印电子表格文档。
组合实现多态性
在经典的面向对象设计里面,多态性传统上是通过继承实现的,然而组合也可以实现多态性。我们将在第12章中讨论这一点。