Java重新出发--Java学习笔记(五)--继承与组合的火拼

学习过继承之后,觉得继承百般好,就想要在所有地方都去使用继承,难道使用继承就一点缺点也没有吗?
当然是有的,我们在使用任何方法之前都要去考虑一下这个方法是不是适用。那么在什么情况下我们应该用继承呢。
我们先来了解一下类与类之间的关系

java中类与类之间的关系

我们大部分初学者可能只是了解继承,但实际上类与类之间的关系有5种
--继承(实现),依赖,关联,聚合,组合

下面简单对这些关系做一个解释和介绍:

继承(实现)

对于类来说,这种关系叫做继承;对于接口来讲就叫做实现了。
继承在上一篇做了一个比较详细的学习,这里就不重复了。
接口很多人也都了解是怎么一回事,并且后面还会单独讲解接口,这里就不赘述了。
简单来讲,继承(实现)就是一种“is--a”的关系

依赖

依赖其实是我们在编代码中,每天都会遇到的。简单来讲就是一个类A中使用到了另一个类B中的方法。
这种使用是具有偶然性、临时性非常弱的影响。但是如果被使用的类中发生了变化,使用者自然也就收到了相应的影响。
举个简单的例子,现在我要做一个我写字的功能,这里我就需要new一个代表我自己的类,和一个代表笔的类,接着‘我’调用‘笔’中的写字方法来进行‘我’的写字方法。

public class Pen {
    public void write(){
        System.out.println("use pen to write");
    }
}
public class Me{
    public void write(Pen pen){
        pen.write();
    }
}

这种就叫做依赖。一般而言,依赖关系在JAVA中体现为局域变量、方法的形参,或者对静态方法的调用。

关联

关联体现的是两个类、或者类与接口之间语义级别的一种强依赖关系。这种关系比依赖更强,不存在偶然性和临时性,一般是长期性,双方的关系一般是平等的。关联可以是单项,也可以是双向。
还是用代码举例子:

//pen还是上面例子的pen
public class You{
    private Pen pen;//让pen成为you的类属性
    public You(Pen p){
        this.pen = p;
    }
    
    public void write(){
        pen.write();
    }
}

被关联类B以类属性的形式出现在关联类A中,或者关联类A引用了一个类型为被关联类B的全局变量的。 这种关系,就叫关联关系。

在Java中,关联关系一般使用成员变量来实现。

这里补充一个知识点,成员变量/全局变量/局部变量
成员变量:

  • 写在类声明的大括号中的变量,我们称之为 成员变量(属性,实例变量)
  • 成员变量只能通过对象来访问。
  • 成员变量不能离开类,离开类之后就不是成员变量了。成员变量不能在定义的同时进行初始化。
  • 存储在当前对象对应的堆的存储空间中。
  • 存储在堆中的数据不会被自动释放,要程序员手动释放。

全局变量:

  • 写在函数和大括号外部的变量, 我们称之为全局变量
  • 作用域: 从定义的那一行开始, 一直到文件末尾
  • 全局变量可以先定义在初始化, 也可以定义的同时初始化
  • 存储: 静态区
    程序一启动就会分配存储空间, 直到程序结束才会释放(也有说在JAVA中,全局变量和成员变量是一个意思的,但是我看这二者存放的区域不同,我想这里的全局变量可能指的是静态成员变量吧)

局部变量:

  • 写在函数或者代码块中的变量, 我们称之为局部变量
  • 作用域: 从定义的那一行开始, 一直到遇到大括号或者return
  • 局部变量可以先定义再初始化, 也可以定义的同时初始化
  • 存储 : 栈
  • 存储在栈中的数据有一个特点, 系统的垃圾回收会自动给我们释放

言归正传继续说下一个类关系。

聚合

聚合是关联关系的一种特例,他体现的是整体与部分的关系,可以理解为是has--a的关系

public class House{
    private List Furnishings ;//一个房间里有很多家具
    
    //...
}

在代码的表现上来看,聚合和关联是一致的,只能从语义上对二者进行区分。普通的关联关系,a类和b类没有必然的联系,而聚合中则表达一个从属关系,指b是a的一部分。比如房间里有家具,家庭里有孩子。但是这个从属关系不是必须的,整体与部分之间是可分离的,他们有自己各自的生命周期,部分可以从属于多个整体对象,也可以被多个整体共享。
关联是相对平等的关系地位,而聚合中两者的关系是不平等的。

组合

组合同聚合一样,也是体现了整体于部分之间的关系,唯一不同的是整体与部分是共同进退的。
例子:

public class Person {
    private Eye eye = new Eye();//一个人有鼻子有眼睛
    private Nose nose = new Nose();
    
    //...
}

很显然这里的从属关系当然不能单独存在,只看代码是无法区分关联、聚合和组合的,具体是哪一种关系只能通过语义级别来区分了。
在组合中,两个类的关系也不是平等的。

聚合、组合和继承

依赖关系是每一个JAVA程序都离不开的,也就不单独讨论了。普通的关联关系也不容引起歧义,所以我们重点讨论一下组合、聚合和继承。

聚合与组合

聚合与组合都是一种关联关系,只是额外具有整体-部分的意义。部件的生命周期不同。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时间共享同一个部件。

这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。聚合关系是【has-a】关系,组合关系是【contains-a】关系。平时我们只讨论组合和继承的时候,认为组合是【has-a 】关系,而事实上,聚合才是真正的【has-a】关系,组合是更深层次的【contains-a】关系。由于【contains-a】关系是一种更深的【has-a】关系,所以说组合是【has-a】关系也是正确的。

组合和继承

这个是本文要讨论的重点,在设计模式中有提到,“少用继承,多用组合”,这究竟是为什么呢?
我们来分析一下继承和组合的优缺点先:

组合:
优点:

  • 不破坏封装,整体类与局部类松耦合,彼此相对独立。

  • 具有较好的可扩展性

  • 支持动态组合。在运行时,整体对象可以选择不同类型的局部对象

  • 整体类可以对局部类进行包装。封装聚部类的接口,提供新的接口

    缺点:

  • 整体类不能自动获得和聚部类同样的接口

  • 创建整体类的对象时,需要创建所有局部类的对象

    缺点分析:
    1.整体类不能自动获得和局部类同样的接口:
    如果父类的方法子类中几乎都要暴露出去,这时可能会觉得使用组合很不方便, 使用继承似乎更简单方便。但从另一个角度讲,实际上也许子类中并不需要暴露这些方法,客户端组合应用就可以了。所以上边推荐不要继承那些不是为了继承而设计的类,一般为了继承而设计的类都是抽象类。
    2.创建整体类的对象时,需要创建所有局部类的对象:
    这个可能没什么更好的办法,但在实际应用中并没有多出多少代码。

继承:
优点:

  • 子类能自动继承父类的接口
  • 创建子类对象时,无需创建父类对象

缺点:

  • 破坏了封装
  • 子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
  • 支持扩展,但是往往以增加系统结构的复杂度为代价
  • 不支持动态继承,在运行时,子类无法选择不同的父类
  • 子类不能改变父类的接口

缺点分析:
1.这里再补充说明一下为什么说继承破坏了封装性,我相信很多人看到这里也和我一样一知半解。
首先继承来的方法和属性修饰符会不同
封装:通过共有化方法访问私有化属性,使得数据不容易被任意篡改,常用private修饰属性。
继承:通过子类继承父类从而获得父类的属性和方法,正常情况下正常情况下,用protected修饰属性,
专门用于给子类继承的,权限一般在本包下和子类里;
继承破坏了封装:是因为属性的访问修饰符被修改了,使得属性在本包和子类里可以任意修改属性的
数据,数据的安全性从而得不到保障。
2.为什么继承紧耦合
当作为父类的BaseTable中感觉Insert这个名字不合适时,如果希望将其修改成Create方法,那使用了子类对象Insert方法将会编译出错,可能你会觉得这改起来还算容易,因为有重构工具一下子就好了并且编译错误改起来很容易。但如果BaseTable和子类在不同的程序集中,维护的人员不同,BaseTable程序集升级,那本来能用的代码忽然不能用了,这还是很难让人接受的.
3.为什么继承扩展起来比较复杂
比如有多个子类继承同一个父类,但是不同子类可能会有自己独立的特性,如果要根据继承去实现,可能会引入大量的继承。如果子类继续增加,不同的特性也继续增加,那继承的层次会非常复杂,而且很难控制,而使用组合就能很好的解决这个问题
4.继承不能支持动态继承
这个其实很好理解,因为继承是编译期就决定下来的,无法在运行时改变,如3例中,如果用户需要根据当地的情况选择计税方式,使用继承就解决不了,而使用组合结合反射就能很好的解决。
5.为什么继承,子类不能改变父类接口
比如2中举得例子,子类中觉得Insert方法不合适,希望使用Create方法,因为继承的原因无法改变。

组合与继承的区别和联系:
在继承中,父类的内部细节对于子类是可见的,所以我们通常也可以说通过继承的代码复用是一种白盒式复用。(如果基类的实现发生改变,那么派生类的实现也会随之改变。这样会导致子类行为的不可预知性。)
组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用 。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)

继承在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)
组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。组合(has-a)关系可以显式地获得被包含类(继承中称为父类)的对象,而继承(is-a)则是隐式地获得父类的对象,被包含类和父类对应,而组合外部类和子类对应。

组合是组合类和被包含类之间的一种松耦合关系,而继承则是父类和子类之间的一种紧耦合关系。选择了组合关系时,组合类可已选择调用外部类必须的方法,而使用继承关系时,子类要无条件的继承父类所有的方法和属性。

讲了这么多好像都是组合的好处,难道继承真的一无是处吗?其实也不是,有一点很重要的是,使用继承关系时可以实现类型的回溯,即用父类变量引用子类对象。这样便可以实现多态,而组合是没有这个特性的。

但是还有一点需要注意,如果你确定复用另外一个类的方法永远不需要改变时,应该使用组合,因为组合只是简单地复用被包含类的接口,而继承除了复用父类的接口外,它甚至还可以覆盖这些接口,修改父类接口的默认实现,这个特性是组合所不具有的。

从逻辑上看,组合最主要地体现的是一种整体和部分的思想,例如在电脑类是由内存类,CPU类,硬盘类等等组成的,而继承则体现的是一种可以回溯的父子关系,子类也是父类的一个对象。这两者的区别主要体现在类的抽象阶段,在分析类之间的关系时就应该确定是采用组合还是采用继承。引用网友的一句很经典的话应该更能让大家分清继承和组合的区别:组合可以被说成“我请了个老头在我家里干活” ,继承则是“我父亲在家里帮我干活"。

继承还是组合?

首先它们都是实现系统功能重用,代码复用的最常用的有效的设计技巧,都是在设计模式中的基础结构。很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。从前面的介绍已经优缺点对比中也可以看出,组合确实比继承更加灵活,也更有助于代码维护。所以,建议在同样可行的情况下,优先使用组合而不是继承。因为组合更安全,更简单,更灵活,更高效。

但这并不是说继承就不能使用了,前面说的是【在同样可行的情况下】。有一些场景还是需要使用继承的,或者是更适合使用继承。继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在 is-a 关系的时候,类B才应该继承类A。

向上转型这件事情会在后面的文章中进行详细讲解。

总结

根据前面所讲的我们好像发现继承缺点大于优点,尽管在OOP的学习中,不断的重复继承,但是这并不意味着我们要处处使用继承,相反要十分慎重的使用它。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。 继承最大的优点就是扩展简单,但大多数缺点都很致命,但是因为这个扩展简单的优点太明显了,很多人并不深入思考,所以造成了太多问题。

1、精心设计专门用于被继承的类,继承树的抽象层应该比较稳定,一般不要多于三层。
2、对于不是专门用于被继承的类,禁止其被继承。
3、优先考虑用组合关系来提高代码的可重用性。
4、子类是一种特殊的类型,而不只是父类的一个角色
5、子类扩展,而不是覆盖或者使父类的功能失效

你可能感兴趣的:(Java重新出发--Java学习笔记(五)--继承与组合的火拼)