我们费尽心思做出一种数据类型后,假如不得不新建另外一种类型,令其实现大致相同的功能,那会是一件非常令人灰心的事情,毕竟“重复是魔鬼”。若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,则修改过的“克隆”类(正式名称叫作派生类或者继承类或者子类)也会反映出这种变化。
在Java语言中,继承是通过extends关键字实现的。使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有成员(尽管private成员被隐藏起来,且不能访问),但更重要的是,它复制了基础类的接口。也就是说,可向基础类的对象发送的所有消息亦可原样发给衍生类的对象。根据可以发送的消息,我们能知道类的类型。这意味着派生类具有与基类相同的类型!
由于基类和派生类具有相同的接口,因此那个接口必须进行特殊的设计。也就是说,对象接收到一条特定的消息后,必须有一个“方法”能够执行。若只是简单地继承一个类,并不做其他任何事情,来自基类接口的方法就会直接照搬到派生类。这意味着派生类的对象不仅有相同的类型,也有同样的行为,这一后果通常是我们不愿见到的。图3-5展示了基类和派生类的关系。
图3-5 基类和派生类
有两种做法可将新得的派生类与原来的基类区分开。第一种做法十分简单:为派生类添加新函数(功能)。这些新函数并非基础类接口的一部分。进行这种处理时,一般都是意识到基类不能满足我们的要求,所以需要添加更多的函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的问题。然而,事先还是要仔细调查自己的基类是否真的需要这些额外的函数。
尽管extends关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定如此。为区分我们的新类,第二个办法是改变基类一个现有函数的行为。我们将其称作“改善”那个函数。为改善一个函数,只需为衍生类的函数建立一个新定义即可。我们的目标是:“尽管使用的函数接口未变,但它的新版本具有不同的表现”。
针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若答案是肯定的,则派生类型就是与基类完全相同的类型,因为都拥有完全相同的接口。这样造成的结果就是:我们完全能够将派生类的一个对象换成基类的一个对象!可将其想象成一种“纯替换”。在某种意义上,这是进行继承的一种理想方式。此时,我们通常认为基类和派生类之间存在一种“等价”关系——因为我们可以理直气壮地说:“哈士奇就是一种狗”。为了对继承进行测试,一个办法就是看看自己是否能把它们套入这种“等价”关系中,看看是否有意义。
在许多时候,我们必须为派生类型加入新的接口元素。所以不仅扩展了接口,也创建了一种新类型。这种新类型仍可替换成基类,但这种替换并不是完美的,因为不可在基类里访问新函数。我们将其称作“类似”关系;新类型拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个例子来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各种控制器;也就是说,我们已拥有必要的“接口”来控制制冷。现在假设制冷机出了故障,我们把它换成一台新型的冷、热两用空调,冬天和夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷的设备,因此它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩展,但现有的系统并不知道除原始接口以外的任何东西。
认识了等价与类似的区别后再进行替换时就会有把握得多。尽管大多数时候“纯替换”已经足够,但你会发现在某些情况下仍然有明显的理由需要在派生类的基础上增添新功能。通过前面对这两种情况的讨论,相信大家已心中有数。
另外一个是“Shape”示例,基类是“Shape”,每个Shape都有尺寸、颜色、位置等,并且都可以画、清除、移动、上色等。特定Shape的派生类型circle、square、triangle等,都可能有自己的特征和行为。例如,某些Shape可以翻转,有些行为可能会有所不同,比如,计算一个Shape的面积(见图3-6)。
图3-6 Shape类型
有2种方法可以区分原来的基类和新派生类。
第一种非常简单:只需向派生类添加新的方法(见图3-7)。这些新方法不是基类接口的一部分。这意味着基类没有你所希望的方法,所以增加了更多的方法。这个是简单而原始的继承使用,你的问题可能会得到完美的解决,另外,你的基类可能也需要这些额外的方法。在面向对象程序设计中,经常会出现这种发现和迭代。
图3-7 向派生类添加新的方法
虽然继承可能有时意味着将添加新的方法到接口,但是这不一定总是对的。第二种更重要的方式添加新类来改变现有基类方法的行为,被称为“覆盖(overriding)”(见图3-8)。
图3-8 覆盖
覆盖的方法只需为派生类中的方法创建一个新的定义:“使用的是相同的接口方法,但是会在新类型里做不同的事情”。
不同种类的对象往往有一定量的共同点。例如,哈士奇(Husky)、贵宾犬(Poodle),所有的狗都有共同的特点:有颜色、有名字、会叫唤、会摇尾。然而,每种狗都有额外的差异,比如颜色不同、名字不同、叫声不同。
面向对象的编程允许类从其他类继承常用的状态和行为。在这个例子中,Dog现在变成了Husky和Poodle的超类。在Java编程语言中,每一个类被允许具有一个直接超类,每个超类具有无限数量的子类的潜力,示例如图3-9所示。
图3-9 继承的示例
继承使用extends关键字: