本文共 4608 字,大约阅读时间需要 15 分钟。
Don’t reinvent wheels (不要重新发明车轮 )。 和 Java 面向对象编程的特性一样,继承( inheritance )在 Java 也是与生俱有的。所有类,无论是 API ,还是编程人员自己编写的,都自动继承于 Java 所有类的始祖—— Object 类。可以说, Object 代表了所有类的共性。在这个章节,我们将首先阐述继承的概念,解释为什么继承在 Java 编程中如此重要。然后一步步地用实例讨论如何实现继承,以及继承应用中的各种编程技术。 7.1 概述
继承是面向对象设计和编程中最本质的特点之一。和现实世界中的继承概念一样,继承就是代码重用。通过继承并扩充已存在或编好的类,例如 API 类,解决应用程序中的具体问题。 从代码编写的角度,继承技术并不难应用,关键在于概念理解、归类分析,以及正确使用。列举一个模拟各种汽车运行操作的编程例子。首先,我们可能想到不同类型的汽车:小轿车、大卡车、跑车、吉普车等等。我们当然不会对每一类型车都从头到尾编写整套模拟程序。经过归类分析,得到如下结论: 1. 无论什么类型汽车都有共同的零部件,如车轮、引擎、方向盘、车座、传动,以及排气等等。我们已经知道,在程序设计中,它们可以由状态( state )和行为( behavior ),或类中的变量和方法来表示。
2. 不同类型汽车在这些共同零部件和系统的基础上,继承、扩充、甚至改进而来。例如,跑车的引擎启动快、加速高;小轿车舒适和易于操纵;而大卡车马力大等等。在代码编写中,有了对引擎一般特征的定义,在编写各类汽车时,对引擎的部分变量和方法进行添加和修改(重载和覆盖),这便是继承。这些分析可以通过图 7.1 来表示 (见书第七章) 。 综上所述,继承在编程中是指两个或者多个类之间存在的特定关系——定义、功能,以及逻辑上的内在联系。我们称作为继承的基础、涵盖子类的类为超类( superclass ),称从超类导出或继承而来的类为子类( subclass )。在下面的章节可以看到,继承往往是多级的。超类和子类之间这种继承关系实际上是“ is a ”,或者“是”的关系。这种“ is a ”关系的内涵指示我们一个超类一定存在可以被子类重新利用的代码,所以不必再“重新发明车轮”,而是把它们作为“新型车”零部件,再增添新的“零件和功能”,使之成为解决新问题应用软件。 “ is a ”关系也提示我们,除过所有的类都有共同的超类 Object 之外,并不是类和类之间都存在继承关系。有可能是“ has a ”关系,即“有”关系。在“ has a ”关系的类中应用继承就毫无意义,因为它们之间不存在定义、功能,以及逻辑上的内在联系。它们是互相支持的关系。因而具有“ has a ”关系的类之间不可能产生代码重用。 正确应用继承技术,涉及对要解决问题的归类分析,要求明确类之间存在“ is a ”关系,并确定超类的共性和子类的个性范畴。 这样才可达到代码重用、增强可靠性、简化程序设计、提高编程效率、并使之易于维护的目的。如下小节对这些概念作专门讨论。 7.1.1 归类分析
归类分析, analysis of categorization ,与要解决的具体问题紧密相连。给定一个编程问题,例如设计一个统计打印学生成绩单的程序,如同上面小节分析汽车的例子,首先,我们从学生这个类入手。综合和概括( generalization )学生成绩单的共性是归类分析的重要一步。无论什么学生,成绩单一定包括定义学生对象的变量,如姓名、学号、住址、专业、学分等等;也一定包括统计、计算、列表打印等方法。这些分析为超类的设计奠定了基础。接着我们着手分析学生的特性( specialization ),归纳学生的不同类型,如本科生、研究生、在职进修生、旁听生等等。进一步分析学生类型的结果,导致产生子类以及类之间的“ is a ”关系。例如研究生类又分为硕士和博士等。最后,对每个学生类的实例变量、类变量,以及方法签名的分析,使得我们的设计分析更加具体,利用继承技术编写代码则成为水到 渠成。 当然,对超类的归类分析还涉及到抽象化和接口化,导致利用多态( polymorphism )的可能性。我们将在第 8 和第 9 章专门讨论这些编程概念和技术。另外,归类分析,尤其是对子类的分析,还涉及到多重分离( multiple partitioning )、强化特性( strengthening specialization )等模块化设计手段。这些面向对象设计的主题,超出本书的讨论范围。感兴趣的读者朋友可参考有关这方面的书籍。 7.1.2 “is a”关系和“has a”关系
假设有两个类: Computer 和 Employee 。明显地,这两个类之间不存在“ is a ”的关系,即 Employee 不是计算机,它们之间没有继承关系的必要。因此不可能产生代码重用性。但这两个类之间是“ has a ”关系,即是支持的关系。例如, Employee “ has a ” Computer 。明显地是一种支持关系。这种支持关系落实到代码中,就是在 Employee 中创建 Computer 的对象,调用其方法,到达完成某种运算和操作的目的。 Employee 和 Manager 类存在的则是“ is a ”关系,即 Manager 是 Employee 。它们之间存在共性,或者共同的属性。 Manager 是 Employee 的具体化; Employee 是 Manager 的概括和抽象。概括性和抽象性的类,如 Employee ,在继承中则定义为超类。具体或代表对象特性的类,如 Manager ,则定义为子类。如果这是一个用来计算雇员工资的程序,那么在超类 Employee 中,我们应当包括所有子类都应该具有的、与计算工资有关的数据,例如 name 、 employeeID 、 jobTitle 、 seniority 、 baseSalary 以及用来计算基本工资部分的方法,如 baseSalary() 等。在 Manager 这个子类中,我们不仅继承 Employee 的所有数据和方法,还增加针对 Manager 的新的数据,如是否董事会成员 boardMember 、职务补贴 merit 等,因为除基本工资的计算之外,这些都影响到具有经理职务雇员的收入。 对两个类之间“ is a ”或是“ has a ”关系的分析,有助于我们确定它们之间是否存在继承关系,避免设计上的错误,因而达到提高代码重用性的目的。 7.1.3 代码重用
代码重用性( code-reusability )是指在存在继承关系的类中,超类中已经存在的代码,包括数据和方法,可以继承到子类中。子类通过继承获得了这些代码,就可以像自己的“财产”或属性( properties )一样使用。不仅如此,子类还可以增添新的数据和方法,使之功能更强大,解决的问题更具体。必须指出的是,如果一个子类继承了超类,而没有使用超类中的任何数据和方法,这种继承是没有意义的,因为它没有体现继承的代码重用性。 在继承中,一个子类可以有一个或者多级超类,形成一个继承链。如图 7.2 中所示(见书第七章)。紧挨着子类 Subclass 上面的超类被称作直接超类,或者父类;反过来讲,紧接着超类的子类被称作直接子类。沿着这个继承链再往上走,所有这些类都是子类的超类,或称间接超类,或者祖类。当然,最上面是所有类的祖始 Object 。在继承中,一个子类继承了所有在该继承链上的父类和祖类的属性——数据和方法,再加上自己新增添的数据和方法,使之功能更强大,解决的问题更具体。子类在继承中是相对而言的术语。一个子类很可能是下一级继承的超类。 在继承链中从上往下,以设计的角度,是一个从抽象到具体的过程;从编程的角度,是一个从少至多的过程。 Object 类之所以是所有类的超类,是因为它高度概括、高度抽象,包括了所有类的共性,代表了所有类的属性。编程人员在设计编写继承时,也同样遵循这样一个原则,从抽象到具体,从共性到特性,即超类代表所有其子类共有的数据和方法;子类继承这些属性,并可能增添代表它自己对象特性的数据和方法。 这个设计原则保证继承的效率,提高代码的重用性。 7.1.4 代码可靠性
代码公开、免费应用的 Java API 类给软件开发展示了无限机遇,也伴随着困惑和挑战。怎样在这个“宝库”中找到解决具体问题的 API 类,或者找到可以作为超类的 API 类,所以我们在编程中不必再“重新发明车轮”呢?答案是没有捷径可走。只有多看、多写、多思考,利用已经编好的、并且被实践检验过的例子作为楷模,并更新修改后,变成能够解决您的实践问题的代码。 Java 编程的这一特点实际上蕴含着不可多得的益处——提高代码的可靠性。充分利用 API 类可以提高代码的可靠性是显而易见的。这些 API 由职业语言开发者编写、经过多年的修改、改进、和完善、经过广泛的不同规模和层次的实际应用,代码的可靠性应该毋庸置疑。我们在继承中利用 API 类,就如同“站在巨人的肩膀上”一样,除代码重用之外,还大大提高了应用软件的可靠性。 7.1.5 其他好处
应用继承可以简化程序设计、提高编程效率,以及增强程序的维护性。 简化程序设计——继承 API 类的好处不言而喻。例如,编写一个有按钮、菜单、鼠标控制的窗口绘图软件,如果不继承 JDK 提供的 JFrame 、 JButton 、 JMenu 、 Graphics 等 API 类,其编程难度可想而知。 提高编程效率——简化程序设计必然提高编成效率。如上例,我们不再为如何编写显示窗口的 JFrame 代码而伤透脑筋;也不必为如何控制鼠标的移动( MouseMoveListener )而煞费苦心。 增强程序维护性——继承体现面向对象编程的本质:模块化设计和封装性。对程序的更新改进,只涉及到相关的类,而不是整个程序体。正确地运用继承,对代码的修改不会产生“触一发而动全身”的效应。封装性的功能使我们能够做到不用了解具体代码详情就可使用的效果,大大简化和缩小了维修目标和纠错范围。例如,我们会将对程序的维护集中在继承而来的子类上,而不是作为超类的 API 类。 7.1.6 局限性
1. 在多线程并行处理中,继承中的子类对象可能导致线程间的不协调问题。我们将在第 14 章专门讨论多线程编程。 2. Java 只支持单独超类继承,而不允许综合继承( multiple inheritance )。但可以应用间接多继承。我们将在 7.1.8 小节讨论继承类型。 3. 一旦实例化,对象不能够转换角色。例如,学生对象不可能再转换成为教师。 4. 数据安全问题。例如,某个对象需要访问继承而来的超类数据,必须给予所有对象访问这些数据的访问权。 这些局限性无疑对程序设计和代码编写提出了更高要求。 本文转自高永强51CTO博客,原文链接: http://blog.51cto.com/yqgao/171656 ,如需转载请自行联系原作者