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

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

按惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。这段描述信息要以这样的句子开头:“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,或者包级私有并增加一些静态工厂来替代构造器。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容