面对对象之继承及其弊端(Java)

继承的概念

继承是使用已存在的类定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。从对客观世界的认识角度来看,继承体现的是客观事物之间的层次关系。这种技术是实现代码重用的有力手段,减少软件开发的时间。

当多个类之间存在相同的属性或方法时,可以从这些类中抽象出父类(或称为基类、超类),在子类(或称为导出类、继承类)无需重新定义这些属性和方法,而只需通过extends语句来声明继承父类。父类包含所有子类所共享的特性和行为,子类则继承父类的成员:

  • 如果子类和父类位于同一个包中,子类继承父类中的public、protected和包访问级别的成员变量和成员方法。
  • 如果子类和父类位于不同的包中,子类继承父类中的public和protected的成员变量和成员方法。

其实其他成员也是有的,只是被隐藏了起来,并且不可访问。

实际上子类是父类的特殊化,它除了拥有父类的特性外,还拥有自己独有的特性。例如鱼会游、鸟会飞,这两者拥有其他动物没有的特性。同时在继承关系中,子类复制了父类的接口,也就是说可以发给父类对象的消息同时也可以发给子类对象,由于通过发送给类的消息的类型可得知类的类型,所以这也就意味着子类与父类具有相同的类型,这里通过继承产生了类型的等价性,子类对象完全可以替换父类对象,反之则不可以,例如一个圆形就是一个几何形,但是却不能说一个几何形就是一个圆形。

如果子类对象不仅与父类对象具有相同的类型,而且还拥有相同的行为,那么这样的继承就没有意义了,子类应该是与父类具有一定的差异的,让子类与父类的行为产生差异有两种方法:

  • 直接在子类中添加新的方法。在这样做的时候应该考虑是否存在父类需要增加这些新方法的可能性。
  • 改变父类的方法的行为,也就是改变父类方法的实现,这被称为方法覆盖。

如果只是覆盖父类的方法,而没有添加新的方法,那就意味着子类与父类是完全相同的类型,它们具有完全相同的接口,结果可以用一个子类对象来完全替代一个父类对象,这可以视为纯粹替代,通常称为替代原则。我们经常将这种情况下的子类与父类之间的关系称为is-a(是一个)关系。判断是否继承,就是要确定是否可以用is-a来描述类之间的关系,并使之有实际意义。
而有时候子类的行为在父类中并没有,是子类特有的行为,这时,需要在子类中添加新的行为,这样也就扩展了接口。但是这个子类仍然可以替代父类,只是在父类不能访问新的方法,这种关系我们描述为is-like-a(像是一个)关系。

构造器

提到继承,就不得不提及构造器,一个类能被继承的条件不止是该类没有被final关键字修饰,而且它必须具备一个能被子类访问的构造器。

构造器不会被继承,但是可以通过super()被访问,在使用该类构建对象时,从父类开始向子类一级一级地完成构建。虽然我们并没有显示的引用父类的构造器,但是编译器会默认给子类调用父类的默认构造器(默认构造器指的是没有形参的构造器,如果你没有自己定义一个构造器,那么,编译器会自动帮你创建默认构造器),如果父类没有默认构造器,我们就要必须显示的使用super()来调用父类构造器,否则编译器会报错:无法找到符合父类形式的构造器,而且调用父类构造器必须是在子类构造器中做的第一件事情,也就是第一行代码。

对于子类而已,其构造器的正确初始化是非常重要的,而且当且仅当只有一个方法可以保证这点:在构造器中调用父类构造器来完成初始化,而父类构造器具有执行父类初始化所需要的所有知识和能力。

继承的弊端

继承最大的弊端是打破封装。每个类都应该封装它的属性及实现,这样,当这个类的实现细节发生变化时,不会对其他依赖它的类造成影响,但是在继承关系中,子类与父类之间是强耦合关系,子类依赖于其父类中特定功能的实现细节,如果父类发生变化,子类有可能遭到破坏,因此,子类必须跟着改变。而且当子类的某些行为依赖与父类特定功能的具体实现时,若要对该行为进行添加或改变时,子类的设计者还必须了解父类特定功能的具体实现细节,否则,子类可能产生错误的行为。

继承树的层次太多而造成对象模型的结构太复杂,难以理解,增加了设计和开发的难度,削弱系统的可扩展性和可维护性。如果选择继承,就应该尽量控制继承树的层次数目。

继承与组合的选择

有时候可以使用组合来避免使用继承所带来的问题,即在新的类中增加一个私有域,它引用现有类的一个实例,这样,现有类变成新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这样得到的类就不依赖现有类的实现细节,即便现有类添加新的方法也不会影响新类的使用。组合关系不会打破封装,使系统具有较好的松耦合性,因此使得系统更加容易维护。

那么如何在组合与继承之间选择呢?

组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情况。即在新类中嵌入某个对象,让其实现所需要的功能,但是新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。

而继承是将某个类特殊化以满足某些特殊需求。如果两个类型确实存在“is-a”的关系,也就是说子类的确是父类的子类型,那么就适合使用继承。

在《Thinking in Java》一书中提到:

到底应该使用组合还是用继承,一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的;但如果不需要,则应当好好考虑自己是否需要继承。

在《Effective Java》中有:

在决定使用继承而不是复合之前,还应该问自己最后一个组问题。对于你正试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把那些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。

设计专门用于被继承的类

在包的内部使用使用继承是安全的,因为子类与父类的实现都处于同一个程序员的控制下,但是进行跨包继承则是非常危险的,如果某个类被设计用来被继承,那么下面有几条建议可以看看。

  1. 对于这些类必须提供良好的文档说明,使得创建该类的子类的开发人员知道如何正确安全地扩展它。该类的文档必须精确得地描述覆盖每个方法所带来的影响,对于每个公有的或受保护的方法,它的文档必须指明该方法调用了哪些可覆盖的方法,是以什么顺序调用的每个调用的结果又是如何影响后续的处理过程的。

  2. 尽可能地封装父类的实现细节。如果某些实现细节必须被子类访问,就该方法定义为受保护类型。

  3. 把不允许子类覆盖的方法定义为final类型。

  4. 父类的构造器不允许调用可被子类覆盖的方法。

防止类被继承

如果某些类不是专门被设计用来被继承的,哪么随意继承它是不安全的,可以采用以下两种措施来防止类被继承:

  1. 把类声明为final类型。

  2. 把该类的所有构造器声明为private类型,然后通过一些静态方法来负责构造自身的实例。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值