美文网首页Effective Java
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

作者: 徐大力Bear | 来源:发表于2017-05-18 01:03 被阅读0次

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

按惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。这段描述信息要以这样的句子开头:“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文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。由此看来,上面的这段文档违背了这一格言,这正是继承破坏了封装性所带来的不幸后果,因为在上面这段文档中它必须要说明清楚调用可覆盖方法所带来的影响。所以,为了设计一个类的文档,以便它能够被安全的子类化,必须描述清楚那些有可能未定义的实现细节。

为了继承而进行设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无需随不必要的痛苦,类必须通过某种形式提供适当的钩子,以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域,后者比较少见。见java,util.AbstractList中removeRange方法

protected void removeRange(int fromIndex,  
               int toIndex)  
  
Removes from this list all of the elements whose index is between fromIndex, inclusive, and toIndex, exclusive. Shifts any succeeding  
 elements to the left (reduces their index). This call shortens the list by (toIndex - fromIndex) elements. (If toIndex==fromIndex,   
this operation has no effect.)  
  
This method is called by the clear operation on this list and its subLists. Overriding this method to take advantage of the internals  
 of the list implementation can substantially improve the performance of the clear operation on this list and its subLists.  
  
This implementation gets a list iterator positioned before fromIndex, and repeatedly calls ListIterator.next followed by ListIterator  
.remove until the entire range has been removed. Note: if ListIterator.remove requires linear time, this implementation requires   
quadratic time.  
  
Parameters:  
    fromIndex - index of first element to be removed  
    toIndex - index after last element to be removed  ()

(从列表中删除所有索引处于fronIndex(含) 和 toIndex(不含)之间的元素。将所有符合条件的元素移到左边(减小索引)。这一调用将从ArrayList中删除(toIndex-fromIndex)之间的元素 。(如果toIndexx==fromIndex,这项操作就无效。)

这个方法是通过clear操作在这个列表及其子列表中调用的。覆盖这个方法来利用列表实现的内部信息,可以充分地改善这个列表及其子列表中的clear操作的性能。

这项实现获得了一个处在fromIndex之前的列表迭代器,并以此地重复调用ListIterator.remove和ListIterator.next,直到整个范围都被移除为止。注意:如果ListIterator.remove需要线性的时间,该实现就需要平方级的时间。

参数:
fromIndex 要移除的第一个元素的索引
toIndex 要移除的最后一个元素之后的索引)

这个方法对于List实现的最终用户并没有意义。提供该方法的唯一目的在于,使子类提供针对子列表(sublist)的快速clear方法。如果没有removeRange方式,当在子列表(sublist)上调用clear方法时,子类将不得不用平方级的时间来完成它的工作。否则,就得重新编写整个subList机制---这可不是件容易的事情。

当为了继承而设计类的时候,并没有法则可以决定应该暴露哪些受保护的方法或者域,所能做到的最佳途径就是努力思想,发挥最好的想像,然后编写一些子类进行测试,应该尽可能少的暴露受保护的成员,因为每个方法或域都代表了一项关于实现细节的承诺。另一方面,又不能暴露的太少,因为漏掉的受保护的方法可能会导致这个类无法被真正用于继承。

对于为了继承而设计的类,唯一的测试方法就是编写子类。如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的痛苦变得更加明显。相反,如果编写了多个子类,并且无一使用受保护的成员,或许应该把它做成是私有的。经验表明,3个子类通常就足以测试一个可扩展的类,除了超类的创建者外,都要编写民一个或者多个这种子类。

在为了继承而设计有可能被广泛使用的类时,必须要意识到,对于文档中所说明的自用模式,以及对于其受保护方法和域中所隐含的实现策略,实际上已经做出了永久的程度。这些承诺使得你在后续的版本中提高这个类的性能或者增加新功能都变得非常难,甚至不可能。因此,必须在发布之前先编写子类对类进行测试。

为了允许继承,类还必须遵守其他一些约束。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果违反了这条规则 ,很可能导致程序失败。超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用 。如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作,该方法将不会如预期般的执行。

public class Super {
    public Super(){
        overrideMe();
    }
    public void overrideMe(){  
    
    } 
}
public final class Sub extends Super{
    private final Date date;
    
    Sub(){
        date = new Date();
    }
    public void overrideMe(){
        System.out.println(date);
    }
    
    public static void main (String[] args){
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

打印的结果第一次是null,因为overrideMe方法被Super构造器调用的时候,构造器Sub还没有机会初始化date域。

对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。有两种办法可以禁止子类化:第一,把这个类声名为final。第二、把所有的构造器都声名为private,或者包级私有并增加一些静态工厂来替代构造器。

如果具体的类没有实现标准的接口,那么禁止继承可能会给有些程序员带来不便,如果认为必须允许从这样的类继承,一种合理的办法就是确保这个类永远不会调用它的任何可覆盖的方法,并在文档中说明这一点。也可以机械的消除类中可覆盖方法的自用特征,而不改变它们的行为。将每个可覆盖方法的代码体移动到一个私有的辅助方法中。并且让每个可覆盖的方法调用它的私有辅助方法,然后在需要自我调用的时候直接去调用这些私有的辅助方法,这样相当于是把可以会被重写的代码复制了一份,而在超类构造时调用的是备份版本。

相关文章

网友评论

    本文标题:第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

    本文链接:https://www.haomeiwen.com/subject/zibjxxtx.html