类的继承(四):为什么说继承是把双刃剑

  1. 继承的强大表现在哪?
    答:

    • 继承广泛应用于各种 Java API、框架和类库之中。
    • 一方面它们内部大量使用继承,另一方面它们设计了良好的框架结构,提供了大量基类和基础公共代码。
    • 使用者可以使用继承,重写适当方法进行定制,就可以简单方便地实现强大的功能。
  2. 继承为什么会有破坏力?
    答:

    • 继承可能破坏封装,而封装可以说是程序设计的第一原则
    • 继承可能没有反映出 is-a 的关系。
  3. 怎样理解封装?
    答:

    • 封装就是隐藏实现细节,提供简化接口
    • 使用者只需要关注怎么用而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。
    • 方法是封装,类也是封装。通过封装,才能在更高的层次上考虑和解决问题
    • 可以说,封装是程序设计的第一原则,没有封装,代码之间会到处存在着实现细节的依赖,则构建和维护复杂的程序是难以想象的,可阅读性和可维护性就会变得很差。
  4. 怎样理解继承可能破坏封装?
    答:

    • 因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。
    • 如果子类不知道父类方法的实现细节,它就不能正确地进行扩展
    • 子类和父类之间是细节依赖,子类扩展父类,但仅仅知道父类能做什么还是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随便修改,否则可能影响子类。
    • 更具体地说,子类需要知道父类的可重写方法之间的依赖关系,而且这个依赖关系,父类不能随意改变。但即使这个依赖关系不变,封装还是可能因为细节修改而被破坏。
    • 父类不能随意增加公开方法,因为给父类增加就是给所有子类增加,而子类可能必须要重写该方法才能确保方法的正确性。
    • 总结:对于子类而言,通过继承实现是没有安全保障的,因为父类修改内部实现细节,它的功能就可能会被破坏;对于父类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由
  5. 为什么说继承可能没有反映 is-a 关系?
    答:

    • 继承关系是设计用来反映 is-a 关系的,子类是父类的一种,子类对象也属于父类,父类的属性和行为也适用于子类。
    • 但现实中,设计完全符合 is-a 关系的继承关系是困难的。在 is-a 关系中,重写方法时,子类不应该改变父类预期的行为,但是这是没有办法约束的。
    • 继承是应该被当做 is-a 关系使用的,但是,Java 并没有办法约束。父类有的属性和行为,子类并不一定都适用,子类还可以重写方法,实现与父类预期完全不一样的行为。
    • 对于通过父类引用操作子类对象的程序而言,它是把对象当作父类对象来看待的,期望对象符合父类中声明的属性和行为。如果不符合,则有可能会造成混乱,代码可维护性变差。
  6. 继承既强大又有破坏性,那怎么办?
    答:

    • 避免使用继承
    • 正确使用继承
  7. 怎样避免使用继承?
    答:

    • 使用 final 关键字。
    • 优先使用组合而非继承。
    • 使用接口
  8. final 关键字对于继承关系的影响?
    答:

    • final 方法不能被重写,final 类不能被继承
    • 给方法加 final 修饰符,父类就保留了随意修改这个方法内部实现的自由,使用这个方法的程序也可以确保其行为是符合父类声明的。
    • 给类加 final 修饰符,父类就保留了随意修改这个类实现的自由,使用者也可以放心地使用它,而不用担心一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象。
  9. 使用组合而非继承的优劣是?
    答:

    • 使用组合可以抵挡父类变化对子类的影响,从而保护子类,应该优先使用组合
    • 但组合的问题是,子类对象不能当作基类对象来统一处理了。解决方法是使用接口
  10. 怎样正确使用继承呢?
    答:使用继承大概主要有三种场景:

    • 基类是别人写的,我们写子类

      基类主要是 Java API、其他框架或类库中的类,在这种情况下,我们主要通过扩展基类实现自定义行为,这种情况下需要注意的是:

      • 重写方法不要改变预期的行为。
      • 阅读文档说明,理解可重写方法的实现机制,尤其是方法之间的依赖关系。
      • 在基类修改的情况下,阅读其修改说明,相应修改子类。
    • 我们写基类,别人可能写子类

      我们写基类给别人用,在这种情况下,需要注意的是:

      • 使用继承反映真正的 is-a 关系,只将真正公共的部分放到基类。
      • 对不希望被重写的公开方法添加 final 修饰符。
      • 写文档,说明可重写方法的实现机制,为子类提供指导,告诉子类应该如何重写。
      • 在基类修改可能影响子类时,写修改说明。
    • 基类、子类都是我们写的

      我们既写基类也写子类,关于基类,注意事项和第 2 种场景类似。关于子类,注意事项和第 1 种场景类似。不过程序都由我们控制,要求可以适当放松一些。