大部分的初学者只知道java中两个类之间可以是继承与被继承的关系,可是事实上,类之间的关系大体上存在五种—继承(实现)、依赖、关联、聚合、组合。
接下来,简单的分析一下这些关系。

1.1 继承(实现)

对于类来说,这种关系叫做继承,对于接口来说,这种关系叫做实现。继承是一种“is-a”关系。

1.2 依赖

依赖简单的理解,就是一个类A中的方法使用到了另一个类B。
这种使用关系从语义上说是,是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A。
一般而言,依赖关系在Java中体现为局域变量、方法的形参,或者对静态方法的调用
比如说,我用笔写字,首先需要一个类来代表我自己,然后需要一个类来代表一支笔,最后,‘我’要调用‘笔’里的方法来写字,用代码实现一下:

  1. public class Pen {
  2. public void write(){
  3. System.out.println("use pen to write");
  4. }
  5. }
  6. public class Me {
  7. public void write(Pen pen){//这里,pen作为Me类方法的参数
  8. pen.write();
  9. }
  10. }

1.3 关联

关联体现的是两个类、或者类与接口之间语义级别的一种强依赖关系。
这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的、关联可以是单向、双向的。
在Java中,关联关系一般使用成员变量来实现。

  1. // pen 还是上面的pen
  2. public class You {
  3. private Pen pen; // 让pen成为you的类属性
  4. public You(Pen p){
  5. this.pen = p;
  6. }
  7. public void write(){
  8. pen.write();
  9. }
  10. }

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

1.4 聚合

聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系

  1. public class Family {
  2. private List<Child> children; //一个家庭里有许多孩子
  3. // ...
  4. }

在代码层面,聚合和关联关系是一致的,只能从语义级别来区分。普通的关联关系中,a类和b类没有必然的联系,而聚合中,需要b类是a类的一部分, 是一种”has-a“的关系,即 a has-a b; 比如家庭有孩子,屋子里有空调。

但是,has 不是 must has,a可以有b,也可以没有。a是整体,b是部分,整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。
不同于关联关系的平等地位,聚合关系中两个类的地位是不平等。

1.5 组合

组合也是关联关系的一种特例,他体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合。

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

组合同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束。
就像你有鼻子有眼睛,如果你一不小心结束了生命周期,鼻子和眼睛的生命周期也会结束,而且,鼻子和眼睛不能脱离你单独存在。
只看代码,你是无法区分关联,聚合和组合的,具体是哪一种关系,只能从语义级别来区分。
同样,组合关系中,两个类额关系也是不平等的。

2.组合,聚合和继承

2.1 组合和继承(重点)

学过设计模式的都知道,要“少用继承,多用组合”,这究竟是为什么呢?

(1) 组合和继承的优缺点

  • 组合 优点:1)不破坏封装,整体类与局部类之间松耦合,彼此相对独立 2)具有较好的可扩展性 3)支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 4)整体类可以对局部类进行包装,封装局部类的接口,提供新的接口

缺点:
1)局部类不能自动获得和整理类同样的接口,即: 如果子类要暴露父类的所有方法,这时可能会觉得使用组合很不方便,使用继承似乎更简单方便。 2)创建整体类的对象时,需要创建所有局部类的对象

  • 继承 优点:1)子类能自动继承父类的接口 2)创建子类的对象时,无须创建父类的对象

缺点:
1)破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性,即: 破坏封装:子类可以访问父类的公共的成员变量和方法,可以随意改变父类的实现细节(比如通过方法重写来改变父类的实现方法); 2)支持扩展,但是往往以增加系统结构的复杂度为代价 3)不支持动态继承。在运行时,子类无法选择不同的父类 4)子类不能改变父类的接口
(2) 继承的区别和联系
1)继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种 白盒式代码复用。 (如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性) 组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的, 所以我们也说这种方式的代码复用是黑盒式代码复用 。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)
2)继承是在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的, 因此降低了应用的灵活性。) 组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。
3)组合(has-a)关系可以显式地获得被包含类(继承中称为父类)的对象,而继承(is-a)则是隐式地获得父类的对象,被包含类和父类对应,而组合外部类和子类对应。
4)继承是父类和子类之间的一种紧耦合关系,组合是在组合类和被包含类之间的一种松耦合关系
5)使用继承关系时,父类的所有方法和变量都被子类无条件继承,子类不能选择;
6)最重要的一点,使用继承关系时,可以实现类型的回溯,即用父类变量引用子类对象,这样便可以实现多态,而组合没有这个特性。
7)还有一点需要注意,如果你确定复用另外一个类的方法永远不需要改变时,应该使用组合,因为组合只是简单地复用被包含类的接口,而继承除了复用父类的接口外, 它甚至还可以覆盖这些接口,修改父类接口的默认实现,这个特性是组合所不具有的。
8)从逻辑上看,组合最主要地体现的是一种整体和部分的思想,例如在电脑类是由内存类,CPU类,硬盘类等等组成的, 而继承则体现的是一种可以回溯的父子关系,子类也是父类的一个对象。 这两者的区别主要体现在类的抽象阶段,在分析类之间的关系时就应该确定是采用组合还是采用继承。
9)引用网友的一句很经典的话应该更能让大家分清继承和组合的区别:组合可以被说成“我请了个老头在我家里干活” ,继承则是“我父亲在家里帮我干活”。
(3)继承还是组合
很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。从前面的介绍已经优缺点对比中也可以看出, 组合确实比继承更加灵活,也更有助于代码维护。议在同样可行的情况下,优先使用组合而不是继承。因为组合更安全,更简单,更灵活,更高效。
注意,并不是说继承就一点用都没有了,前面说的是【在同样可行的情况下】。有一些场景还是需要使用继承的,或者是更适合使用继承。
继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。 反之则应该好好考虑是否需要继承。只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在 is-a 关系的时候,类B才应该继承类A。