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

术语:

可覆盖的(overridable):指非final的,公有的或受保护的。


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

        按惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。这段描述信息要以这样的句子开头:“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.

Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection's iterator method 
does not implement the remove method and this collection contains the specified object.
        该文档清楚地说明了,覆盖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
        从上个这个文档中来理解原文中说的钩子指的是什么?看起来,这里的钩子指的是某种为了在特定环境下优化性能而特意提供的可重写的方法,是进入并修改原始类的一个入口,当类中发生事件或调用A,则会使用到当前建议的钩子B,这样,按照需要重写了B中的方法以后,就会在实际操作中可能会提高一些性能等。比如上面这段文档中的方法对于List实现的最终用户没有任何意义。提供这个方法的目的在于,使子类更易于提供针对子列表的快速clear方法,迭代器的remove方法执行是线性级的,当在子列表上调用clear方法时,子类将不得不用平方级时间来完成工作,否则,就得重写整个subList机制,这可不容易。

        AbstractList中的removeRange代码如下:

    protected void removeRange(int fromIndex, int toIndex) {
        ListIterator<E> it = listIterator(fromIndex);
        for (int i=0, n=toIndex-fromIndex; i<n; i++) {
            it.next();
            it.remove();
        }
    }
       clear方法如下:

    public void clear() {
        removeRange(0, size());
    }
        以下是LinkedList的一个remove实现

    public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
        通过追查源码,AbstractList的clear操作是调用removeRange操作的,而removeRange操作又在一个for循环里调用了迭代器的remove操作,而remove操作在AbstractList里的默认操作是抛出不支持操作异常,这就异味着必须在子类里重写这个方法,这是AbstractList实现的迭代器内部基类的remove方法:

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
        以下是AbstractList默认的remove方法

    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
        由此看来,默认的removeRange方法的复杂度主要是取决于继承AbstractList的子类的remove方法的实现。这也就提供了一种针对特定子类的一种灵活性。

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

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

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

        还要注意,因继承而需要的特殊文档会打乱正常的文档信息,普通的文档被设计用来让程序员可以创建该类的实例,并调用类的方法。而特殊文档看起来混用了很多内容。为了允许继承,类还必须遵守其他的一些约束。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。这是因为超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用。如果该覆盖版本的方法信赖于子类构造器所执行的任何初始化操作,该 方法将不会如预期执行,看以下的例子:

public class Super() {
	// Broken - constructor invokes an overridable method
	public Super() {
		overrideMe();
	}
	public void overrideMe() {}
}

public final class Sub extends Super {
	private final Date date;
	
	Sub() {
		date = new Date();
	}
	
	// Overriding method invoked by superclass constructor
	@Override
	public void overrideMe() {
		System.out.println(date);
	}
	
	public static void main(String[] args) {
		Sub sub = new Sub();
		sub.overrideMe();
	}
}
        在这段程序里,我们本来是期望会打出两次date信息,但是事实上第一次打出的是null,为什么?因为在超类Super中调用了被重写的方法OverrideMe();而子类在重写这个方法的时候调用了打印信息,但是问题在于,当超类初始化的时候,子类构造函数还没有执行,所对date还并没有被初始化,还有,如果overrideMe中已调用了任何date中的任何方法,当Super在构造器中调用overrideMe的时候,就会抛出NullPointerException。

        如果决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为非常类似于构造器,所以无论是clone还是readObject都不可以调用可覆盖的方法,不管是直接还是间接。对于readObject方法,覆盖版本的方法将在子类的状态被反序列化前被运行。对于clone方法,覆盖版本的方法则是在子类的clone方法有机会修正被克隆对象的状态之前被运行。无论哪种情况,都不可避免的导致程序失败。在clone方法的情形中,这种失败可能会同时损害到原始的对象以及被克隆的对象本身。

        最后,如果决定为继承而设计的类中实现Serializable,并且该类有一个readResolve或者writeReplace方法,就必须使readResolve或者writeReplace方法成员受保护的方法,而不是私有方法。这是因为私有的方法会被子类不声不响的忽略掉。

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值