阅读 91

《Effective Java》阅读笔记 17要么为继承而设计,并提供文档,要么就禁止继承

面向对象编程,从一开始被洗脑难免在上手写代码时都会首先思考有没有公共方法啊,能不能把两个类抽象成一个父类再继承啊等,慎重使用继承,当要使用继承时一定要在文档注释中写明重写这个方法会给其他方法带来什么影响。书中给出建议如果类并不是为了继承而生,那么这个类应该用final修饰禁止子类化。

1.继承的缺陷

  • 对于为了继承而设计的类,唯一的测试方法就是编写子类。
  • 为了继承,类还必须遵守其他一些约束。构造器绝不能调用可被覆盖的方法,无论是直接调用还是间接调用。
  • 无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。
  • 为了继承而设计的类,对这个类会有一些实质性的限制。

当父类中的某一个方法调用了另一个可以被继承的方法时,如果子类重写了该方法,则会出错。

public class Super{
  public void method1(){
    //todo
  }
   public void method2(){
    //todo
    method1();
  }
}
public class Sub{
  public void method1(){
    //重写该方法
  }
  //这个时候如果Sub的实例调用了method2的时候,就会发生意想不到的错误。因为method1方法已经被重写了。
}
复制代码

所以,如果编写一个可以被继承的类,则必须保证这个类永远不会调用它的任何可被覆盖方法。

2.如何编写为继承而设计的类?

  • (1)对于public或是protected的方法(非final)或是构造器,在文档中要说明它们调用了哪些自己的public或是protected类型的方法(非final),并说明调用顺序,并说明每次调用的作用。
  • (2)文档编写完成,并发布新版本之后,后续的版本不能违背文档中的细节
  • (3)构造器绝对不能调用可被覆盖的方法。
  • (4)对于实现了Cloneable接口的类,在clone方法中不能调用可被覆盖的方法。
  • (5)对于实现了Serializable接口的类,readResolve和writeReplace方法必须是protected的。
  • (6)对于实现了Serializable接口的类,在readObject方法中不能调用可被覆盖的方法。

3.为了继承而设计必须具有良好文档说明

该类的文档必须精确地描述覆盖每个方法所带来的影响。该类必须有文档说明它可覆盖的方法的自用性。对于每个公有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的。更一般的,类必须在文档中说明,在哪些情况下它会调用可覆盖的方法。

按惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。这段描述信息要以这样的句子开头:“This implementation...”。这样的句子不应该被认为是在表明该行为可能会随着版本的变迁而改变。它意味着这段描述关注该方法的内部工作情况

如下,是摘自java.util.AbstractCollection的规范:

public boolean remove(Object o)  
  Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes  
 an element e such that (o==null ? e==null : o.equals(e)), if this collection contains one or more such elements. Returns true if this   
collection contained the specified element (or equivalently, if this collection changed as a result of the call).  
  This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element   from the collection using the iterator's remove method.  

(如果这个集合中存在制定的元素,就从中删除该指定元素中的单个实例(这是项可选的操作)。更一般地,如果集合中包含一个或者多个这样的元素e,就从中删除这种元素,以便(o == null ? e==null:o.equals(e))。如果集合中包含制定的元素,就返回true(如果调用最终改变了集合,也一样)

该实现遍历整个集合来查找制定的元素。如果它找到该元素,将会利用迭代器的remove方法将之从集合中删除。注意,如果由该集合的iterator方法返回的迭代器没有实现remove方法,该实现就会抛出UnsupportedOperationException。)
复制代码

该文档清楚地说明了,覆盖iterator方法将会影响remove方法的行为。而且,它确定地描述了iterator方法返回的Iterator的行为将会怎样影响remove方法的行为。

与此相反的是,在第16条的情形中,程序员在子类化HashSet的时候,并无法说明覆盖add方法是否会影响addAll方法的行为。

对于程序文档的格言:好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。由此看来,上面的这段文档违背了这一格言,这正是继承破坏了封装性所带来的不幸后果,因为在上面这段文档中它必须要说明清楚调用可覆盖方法所带来的影响。所以,为了设计一个类的文档,以便它能够被安全的子类化,必须描述清楚那些有可能未定义的实现细节

4.进一步优化,如何将类设计成不可继承的?禁止子类化

  • 方法一:用final类型来申明这个类。
  • 方法二:将类的构造器申明为private类型的,,或者包级私有的。然后用静态工厂获取到这个类的实例。

你可以机械地消除类中可覆盖方法的自用特性,而不改变它的行为。==将每个可覆盖方法的代码体移到一个私有的“辅助方法”中,并且让每个可覆盖的方法调用私有辅助方法==。然后,用“直接调用可覆盖方法的私有辅助方法”来代替“可覆盖方法的每个自用调用”。

5.总结

推荐阅读《Effective Java》阅读笔记16 复合优先于继承

要设计一个专门为继承而设计的类是十分困难的,所以能用复合就用复合,如果需要被继承的类没有实现类型接口,不能实现复合包装,并且该类还是需要被继承的,那么请不要在可覆盖的方法中调用其它可覆盖方法,或者将想要覆盖的父类方法拷贝一份出来到子类中,做成私有方法,然后用这个私有方法替代调用super.method()(method指的是方法名)。

6.参考文献

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

文章分类
后端
文章标签