67 继承的利弊和使用原则Word文件下载.docx
- 文档编号:19811806
- 上传时间:2023-01-10
- 格式:DOCX
- 页数:10
- 大小:208.09KB
67 继承的利弊和使用原则Word文件下载.docx
《67 继承的利弊和使用原则Word文件下载.docx》由会员分享,可在线阅读,更多相关《67 继承的利弊和使用原则Word文件下载.docx(10页珍藏版)》请在冰豆网上搜索。
Tips
如果继承树上有接口类型,那么应该尽可能地把引用变量声明为继承树上层的接口类型,参见第8章(接口)。
位于继承树上层的类具有以下作用:
定义了下层子类都拥有的相同属性和方法,并且尽可能地为多数方法提供默认的实现,从而提高程序代码的可重用性。
代表系统的接口,描述系统所能提供的服务。
在设计继承树时,首先进行自下而上的抽象,即识别子类之间所拥有的共同属性和功能,然后抽象出共同的父类,位于继承树最上层的父类描述系统对外提供哪些服务。
如果某种服务的实现方式适用于所有子类或者大多数子类,那么在父类中就实现这种服务。
如果某种服务的实现方式取决于各个子类的特定属性和实现细节,那么在父类中无法实现这种服务,只能把代表这种服务的方法定义为抽象方法,并且把父类定义为抽象类。
比如热水器父类可分为电热水器和燃气热水器这两个子类,电热水器和燃气热水器采用不同的方式烧水,在热水器父类中无法提供烧水的具体实现,因此必须把热水器父类定义为抽象类。
publicabstractclassWaterHeating{
/**烧水*/
publicabstractvoidheating();
/**调节水温*/
publicabstractvoidadjust(intlevel);
}
由于继承树上层的父类描述系统对外提供的服务,但不一定实现这种服务,因此把继承树的上层称为抽象层。
在进行对象模型设计时,应该充分地预计系统现在必须具备的功能,以及将来需要新增的功能,然后在抽象层中声明它们。
抽象层应该比较稳定,这可以提高与其他系统的松耦合及系统本身的可维护性。
6.7.3继承关系最大的弱点:
打破封装
继承关系最大的弱点就是打破了封装。
在第1章的1.3.5节(封装、透明)介绍封装时,曾经提到每个类都应该封装它的属性及实现细节,这样,当这个类的实现细节发生变化时,不会对其他依赖它的类造成影响。
而在继承关系中,子类能够访问父类的属性和方法,也就是说,子类会访问父类的实现细节,子类与父类之间是紧密耦合关系,当父类的实现发生变化时,子类的实现也不得不随之变化,这削弱了子类的独立性。
由于继承关系会打破封装,这增加了维护软件的工作量。
尤其在一个Java软件系统使用了一个第三方提供的Java类库的场合。
例如在基于Web的Java应用中,目前都流行使用Apache开源软件组织提供的Struts框架,这个框架的一个扩展点为Action类。
在Struts1.0版本中,Action类有两个方法:
perform()和saveErrors()。
publicActionForwardperform(
ActionMappingmapping,
ActionFormform,
ServletRequestrequest,
ServletResponseresponse)throwsException
protectedvoidsaveErrors(HttpServletRequestrequest,ActionErrorserrors)
在Java应用中,可以创建继承Action类的子类,例如LoginAction,然后在LoginAction类中覆盖Action类的perform()方法,在perform()方法中则会调用saveErrors()方法。
publicclassLoginActionextendsAction{
publicActionForwardperform(…){//覆盖Action类的perform方法
ActionErrorserrors=newActionErrors();
…
saveErrors(request,errors);
//调用Action类的saveErrors()方法
而在Struts1.2版本中,Action类的perform()方法改名为execute()方法,并且saveErrors(HttpServletRequestrequest,ActionErrorserrors)改为:
saveErrors(HttpServletRequestrequest,ActionMessageserrors)
假如希望将现有的Java应用改为使用Struts1.2版本,就必须对自定义的所有Action子类进行修改。
publicActionForwardexecute(…){//把perform()方法改为execute()方法
ActionMessageserrors=newActionMessages();
//把ActionErrors改为ActionMessages
从以上例子可以看出,当对由第三方提供的Struts框架的Action类做了修改时,软件系统中所有Action的子类也要被相应地修改。
由于继承关系会打破封装,这将导致父类的实现细节很容易被子类恶意篡改。
例如以下Account类的withdraw()方法和save()方法分别用于取款和存款。
publicclassAccount{
protecteddoublebalance;
//余额
protectedbooleanisEnough(doublemoney){
returnbalance>
=money;
publicvoidwithdraw(doublemoney)throwsException{//取款
if(isEnough(money))balance-=money;
elsethrownewException("
余额不足!
"
);
publicvoidsave(doublemoney)throwsException{//存款
balance+=money;
它的子类SubAccount覆盖了Account类的isEnough()方法和save()方法的实现,使得该账户允许无限制的取款,并且按照实际存款数额的10倍来存款。
publicclassSubAccountextendsAccount{
protectedbooleanisEnough(doublemoney){//覆盖父类的isEnough()方法
returntrue;
publicvoidsave(doublemoney)throwsException{//覆盖父类的save()方法
balance+=money*10;
以下程序定义了Account类型的引用变量account,实际引用SubAccount实例,根据Java虚拟机的动态绑定规则,account.save()方法和account.withdraw()方法会和SubAccount实例的相应方法绑定。
Accountaccount=newSubAccount();
account.withdraw(2000);
//调用SubAccount实例的withdraw()方法
account.save(100);
//调用SubAccount实例的save()方法
6.7.4精心设计专门用于被继承的类
由于继承关系会打破封装,因此随意继承对象模型中的任意一个类是不安全的做法。
在建立对象模型时,应该先充分考虑软件系统中哪些地方需要扩展,为这些地方提供扩展点,也就是提供一些专门用于被继承的类。
对这种专门用于被继承的类必须精心设计,下面给出一些建议。
(1)对这些类必须提供良好的文档说明,使得创建该类的子类的开发人员知道如何正确安全地扩展它。
对于那些允许子类覆盖的方法,应该详细地描述该方法的自用性,以及子类覆盖此方法可能带来的影响。
所谓方法的自用性,是指在这个类中,有其他的方法会调用这个方法。
例如Account类的isEnough()方法会被save()方法调用,因此子类覆盖isEnough()方法还会影响到save()方法。
(2)尽可能地封装父类的实现细节,也就是把代表实现细节的属性和方法定义为private类型。
如果某些实现细节必须被子类访问,可以在父类中把包含这种实现细节的方法定义为protected类型。
当子类仅调用父类的protected类型的方法,而不覆盖它时,可把这种protected类型的方法看做父类仅向子类但不对外部公开的接口。
例如手机的存储容量,用户可以查看存储容量,但不能修改它,手机的子类可以查看存储容量,也可以修改它。
因此在手机CellPhone父类中定义了如下的存储容量storage属性及相应的访问方法。
publicclassCellPhone{
privatedoublestorage;
publicdoublegetStorage(){returnstorage;
}//对手机使用者及手机子类公开
protectedvoidsetStorage(doublestorage){this.storage=storage;
}//只对手机子类公开
(3)把不允许子类覆盖的方法定义为final类型。
关于final修饰符的用法参见第7章的7.3节(final修饰符)。
对于Account类,可以把它的isEnough()、withdraw()和save()方法都定义为final类型。
privatedoublebalance;
protectedfinalbooleanisEnough(doublemoney){
publicfinalvoidwithdraw(doublemoney)throwsException{//取款
publicfinalvoidsave(doublemoney)throwsException{//存款
(4)父类的构造方法不允许调用可被子类覆盖的方法,因为如果这样做,可能会导致程序运行时出现未预料的错误。
例如以下Base类的构造方法调用自身的method()方法。
publicclassBase{
publicBase(){method();
publicvoidmethod(){}
publicclassSubextendsBase{
privateStringstr=null;
publicSub(){str="
1234"
;
publicvoidmethod(){System.out.println(str.length());
}//覆盖Base类的method()方法
publicstaticvoidmain(Stringargs[]){
Subsub=newSub();
//抛出NullPointerException
sub.method();
运行Sub类的main()方法时,先构造Sub类的实例。
由于在创建子类的实例时,Java虚拟机先调用父类的构造方法(参见第11章的11.2.3节(子类调用父类的构造方法)),因此Java虚拟机先执行Base类的构造方法Base(),然后在这个方法中调用method()方法。
根据动态绑定规则,Java虚拟机调用Sub实例的method()方法,由于此时Sub实例的成员变量str为null,因此在执行str.length()方法时会抛出NullPointerException运行时异常。
(5)如果某些类不是专门为了继承而设计,那么随意继承它是不安全的。
因此可以采取以下两种措施来禁止继承:
l把类声明为final类型。
l把这个类的所有构造方法声明为private类型,然后通过一些静态方法来负责构造自身的实例。
第11章的11.2.5节(构造方法的访问级别)对此做了进一步解释。
6.7.5区分对象的属性与继承
对于进行面向对象设计的新手,比较容易犯的错误是滥用继承关系,根据对象的属性值来分类。
例如根据手机的颜色进行分类,可分为红手机、蓝手机和银白手机,参见图6-7。
图6-7按颜色分类的手机的类框图
以上对象模型是错误的,颜色仅仅是手机的一个属性,不应该根据它的属性值来进行分类。
对于一棵设计合理的继承树,子类之间会具有不同的属性和行为,子类继承父类的属性和行为,并且子类可以比父类拥有更多的属性和行为。
对于红手机、蓝手机、银白手机和手机,除了类的名字不同,它们的属性和行为都相同,因此这样的设计是不合理的。
再看图6-8所示的对书店里书的分类,尽管在现实世界中,这样的分类是合理的,但在对象模型中,这样的设计是错误的。
书的类别仅仅是Book类的一个属性,可以在Book类中定义一个String类型的type属性来表示书的类别,参见图6-9。
图6-8按书的类别分类的Book类的类框图
<
/<
P>
图6-9用String类型的type属性来表示书的类别
以下程序代码创建了一个Java类别的Book对象。
Bookbook=newBook();
book.setType("
Java"
但String类型的type属性无法表达书的类别之间的包含关系,例如科学类别包括数学类别和计算机类别等。
为了能表达这种包含关系,可以定义一个Category类来表示书的类别,用Category类的自身关联关系来表示书的类别之间的包含关系,参见图6-10。
在Category类中,name属性表示书的类别的名字,parentCategory属性表示书的类别所属的父类别,childCategories属性表示书的类别所包含的所有子类别,childCategories属性为java.util.Set类型,在第15章的15.2节(Set(集))介绍了Set集合的用法。
1…n
图6-10用Category类来表示书的类别
在例程6-5的CategoryTester类中,create()方法创建了Science类别、Math类别和Computer类别的Category对象,然后建立了它们的关联关系,最后创建了一个Math类别的Book对象。
main()方法打印Book对象的类别名字。
例程6-5CategoryTester.java
publicclassCategoryTester{
publicBookcreate(){
CategorycategoryScience=newCategory();
//创建Science类型的Category对象
categoryScience.setName("
Science"
CategorycategoryMath=newCategory();
//创建Math类型的Category对象
categoryMath.setName("
Math"
CategorycategoryComputer=newCategory();
//创建Computer类型的Category对象
categoryComputer.setName("
Computer"
//建立Science类型与Math类型的关联
categoryScience.addChildCategory(categoryMath);
categoryMath.setParentCategory(categoryScience);
//建立Science类型与Computer类型的关联
categoryScience.addChildCategory(categoryComputer);
categoryComputer.setParentCategory(categoryScience);
//创建Math类型的Book对象
BookmathBook=newBook();
mathBook.setCategory(categoryMath);
returnmathBook;
BookmathBook=newCategoryTester().create();
System.out.println(mathBook.getCategory().getName());
//打印书的类别
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 67 继承的利弊和使用原则 继承 利弊 使用 原则