02_oo_05_multi_inheritance

第十八章 Java语言为什么不支持多重继承(Multi-Inheritance )

1 简介

继承是面向对象编程思想中的一个重要概念。当一个子类同时继承自多个父类时,这种情况被称为多重继承。有些面向对象编程语言支持多重继承(例如:C++语言),而有些语言则不支持多重继承,Java语言就是其中之一。

在Java语言面世之前,许多项目在开发的过程中,采用了多重继承。但是,这些项目最终不得不大规模的重构代码(Code Refactoring),因为在引入多重继承的同时,也为代码带来了复杂性,使得项目难以维护。

Java语言的设计者从这些项目中汲取了经验,从而决定Java语言不支持多重继承,在语言的层面上避免了因多重继承而带来的复杂性和困难。那么,本章节我们来分析一下多重继承会引起的问题。

2 类(Class)与接口(Interface)

在开始分析之前,我们首先需要区分一下Java语言中的接口(Interface)与类(Class)的概念。

从设计和开发的角度来看,接口(Interface)对应着设计(Design),即:接口确定了某一个模块(一组类)的功能(Be Capable Of)和输入输出(Inputs/Outputs)。但是,接口并不关心实现细节。接口也可以根据功能模块之间的关系进行扩展(接口继承)。

类(Class)包含了实现的细节;这其中包括成员变量(Instance Variable)和成员方法(Instance Method)的实现。Java类只能有一个直接基类(Direct Base Class),但是,类可以同时实现(Implement)多个接口。

3 菱形继承结构(Diamond Inheritance Hierarchy)

在多重继承特性下,菱形继承结构是导致问题复杂化的“罪魁祸首”。面向对象程序设计的一个重要优势是代码复用(Code Reuse);子类可以直接使用父类中定义的成员变量和成员方法。然而,在大型项目中,因为许多其他原因(例如:开发成本)导致了开发人员做出了错误的设计或者决定。有时,开发人员可能为了重用部分代码,而选择了错误的继承结构,从而导致不可避免的菱形继承关系。

图一是一个最经典、最简单的菱形继承关系图。车辆类(Vehicle)是图中的顶层类(Top-Level Class),它定义了一个成员变量brand(品牌名称),和一个成员方法doMaintenance()(做保养服务)。类GasPoweredCar(汽油车)和ElectricCar(电动车)分别继承自Vehicle;它们同时也继承了成员变量brand和成员方法doMaintenance()。在最底层的是HybridCar(混合动力车);它继承自GasPoweredCar和ElectricCar。那么,按照继承的逻辑,HybridCar应该继承GasPoweredCar和ElectricCar的成员变量和成员方法。可是,这两个类的成员变量和方法都继承自Vehicle类。所以,在这里出现了分歧。有些开发人员认为,在HybridCar中实际上应该有两份成员变量brand和两份成员方法doMaintenance(),因为这两份成员变量和方法分别代表着GasPoweredCar和ElectricCar两个类。然而,另一些开发人员认为,HybridCar只应拥有一份成员变量和方法,因为它们均来自Vehicle类。

为了解决这个问题,C++引入了“虚拟继承”的概念。如果在多重继承的语句中使用了关键字"virtual",则HybridCar只拥有一份成员变量brand;否则,HybridCar会有两份。

图一 菱形继承结构图

图一 菱形继承结构图

图一仅仅只是一个非常简单的菱形继承结构图。如果当看到图二时,可能所有的开发人员都会认为应该避免菱形继承结构。在图二中,增加了CompactCar类和CompactHybridCar类。CompactCar类继承自Vehicle类,所以,它也继承了成员变量brand和成员方法doMaintenance()。CompactHybridCar类继承自CompactCar和HybridCar,所以,它可能会包含三份成员变量brand和成员方法doMaintenance()。

图二 复杂的菱形继承结构图

图二 复杂的菱形继承结构图

4 Java语言体系下的设计

因此,为了避免上述复杂的继承结构,Java从语言设计上屏蔽了多重继承。那么,一个直接的问题是:是不是所有的多重继承的设计都能通过单继承实现呢?答案是肯定的。但是,目前并没有一个方法或者准则能自动的将多重继承关系转换为等价的单继承关系。本文给出一个单继承的设计方法。这个设计并不一定是完美的,但是,它能有效的降低类关系的复杂性,并且在此基础上,如果稍作改进,能有效的提高代码复用度。

在图三中,接口VehicleItf声明了getBrand()和setBrand()方法,用于设置和提取成员变量brand的值;doMaintenance()方法保留在该接口中。类Vehicle定义了成员变量brand,并实现了接口VehicleItf的所有方法。图三还额外定义了GasPoweredItf,ElectricPoweredItf,用于表达汽油驱动和电力驱动机器的接口。PoweredVehicleItf和ElectricVehicleItf接口继承自VehicleItf和GasPoweredItf或者ElectricPoweredItf,用于表达汽油车和电动车的接口。与图一相比,这样做的好处是将GasPoweredItf接口和ElectricPoweredItf接口从VehicleItf的接口中分离开,作为一个独立的接口。而在图一中,它们是紧紧的绑定在GasPoweredCar类和ElectricCar类中的。

类似的,图三还定义了HybridVehicleItf接口,继承自VehicleItf,GasPoweredItf,和ElectricPoweredItf,用于表达混合动力车辆的接口。因此,在此继承图中,GasPoweredCar,ElectricCar和HybridCar分别实现(implement)了各自对应的接口。如果开发人员希望在这三个类中复用Vehicle类中的代码的话,可以在这三个类中定义一个成员变量,类型为VehicleItf接口,但是,实际指向的是一个具体的Vehicle类的实例对象。

如果使用一句话来描述图一与图三设计的区别的话,图三的设计将接口与类做了更加细粒度的分离。这样的分离避免了因为继承而引入的实现内容的复杂化。这也是为什么本文在第二小节单独强调设计与实现(接口与类)的区别。

图三 单继承设计图

图三 单继承设计图

5 结语

本章介绍了为什么Java语言不支持多重继承的原因,并且通过一个车辆设计的例子比较了多重继承与单继承的区别。为了避免代码的过度复杂化,Java语言采用了接口的概念,将设计与实现分离开,为开发人员提供了更加灵活、更加强大的语言特性支持。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.