原文链接
文章也上传到
github
(欢迎关注,欢迎大神提点。)
ITEM 19 为继承设计文档,否则就禁止继承
Item18中讲了没有文档和设计的继承是多么危险,那么一个类为继承被设计和添加文档是什么意思呢?
首先,必须为可重写的方法添加明确的文档。换句话说就是,类必须说明它自己使用的那些可被重写的方法。对每个public和protected方法,文档必须指明那些方法被重写后,或者在将来被改变会对类本身产生的影响。(能被重写我们指的是:nonfinal和public或protected方法)。一般情况下,一个类必须对任何环境下调用的可重写的方法进行文档说明。例如:调用可能来自后台线程或静态初始化。
一般调用了可被重写方法的描述在文档描述后面,被关键字Implementation Requirements指定,这个关键字由Javadoc的@implSpec生成,这里会描述方法内部的实现。这里有个例子,来自java.util.AbstractCollection:
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 Objects.equals(o, 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).
Implementation Requirements:
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 removemethod
and this collection contains the specified object.
这个文档清晰的描述了重写iterator的方法会对remove方法产生的影响以及影响的结果是什么。在Item18中,程序员没有说明重写add方法是否会对addAll方法产生什么影响。一个好的API文档应该仅仅描述方法干了什么,而不应该描述它是如何实现的,那么像上面说的这种文档是不是违反了这种规则呢?答案是肯定的。这是使用继承破坏封装性所带来的不良后果。
这个@implSpec标志在Java8中添加,在Java9中被广泛使用。这个tag应该默认被使用的,但是直到Java9中还没有默认使用,除非在命令行中打开
-tag "apiNote:a:API Note:"。
为了让程序员写出高效的子类而不太痛苦,除了包括自调用的文档。一个类还不得不提供一些恰当的protected方法或者属性。例如java.util.AbstractList类的removeRange方法:
/**
* Removes from this list all of the elements whose index is between
* {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
* Shifts any succeeding elements to the left (reduces their index).
* This call shortens the list by {@code (toIndex - fromIndex)} elements.
* (If {@code toIndex==fromIndex}, this operation has no effect.)
*
* This method is called by the {@code 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 {@code clear} operation on this list
* and its subLists.
*
*
This implementation gets a list iterator positioned before
* {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
* followed by {@code ListIterator.remove} until the entire range has
* been removed. Note: if {@code ListIterator.remove} requires linear
* time, this implementation requires quadratic time.
*
* @param fromIndex index of first element to be removed
* @param toIndex index after last element to be removed
*/
protected void removeRange(int fromIndex, int toIndex) {
ListIterator it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i
当你设计一个继承类的时候,你决定暴漏什么protected方法给子类呢?不幸的是,并没有万能的方法,你能做的就是认真思考,然后多写几个子类来继承你写的类进行测试。你应该尽可能的少暴漏protected成员,但也不能太少,因为太少就限制了子类化。如果你遗漏了一个protected成员,尝试写子类的时候就可能会带来麻烦。相反的,如果几个子类都没有使用到一个成员,那应该将这个成员私有化。
经验表明,3个子类就足够测试一个可继承的类了。当你实现一个用于继承的类时,记得将自用方法添加文档,然后决定公开哪些受保护的方法,但是注意这些公开的方法在将来的版本中可能成为限制你提高性能的承诺。因此,在你公开你的类之前,你必须测试好它。
同时需要注意,专门为继承而写的文档有时候会给正常使用的程序员造成一定的困扰,直到写这篇文章的时候,也没有一个好的工具来把正常的文档和专门为继承而写的文档分开。还有其他的一些需要子类遵守的规则。构造方法不能直接或间接的调用可被重写的方法。如果你违反了这条规则,程序将会失败,因为一个父类的构造方法先于子类的构造方法被调用,子类重写的方法会在子类构造方法调用之前被调用,如果这个被重写的方法的一些初始化在子类的构造方法中就会产生错误。这里给出具体的例子:
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
这里子类重写了方法overrideMe,就会造成父类唯一的构造方法的调用错误。
public final class Sub extends Super {
// Blank final, set by constructor
private final Instant instant;
Sub() {
instant = Instant.now();
}
// Overriding method invoked by superclass
constructor
@Override public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
你可能期望程序打印instant两次,但是第一次打印确实null,因为super的构造方法先于Sub的构造方法被调用,而instant的初始化在子类的构造方法中。还要注意的是,这个程序中观察到final 属性的两种不同状态,这是不是很奇怪?另外,调用instant的地方需要抛出NullPointerException。这里的程序没有抛出这个异常是因为println方法可以接受null的参数。
对于构造方法来说,调用privete方法、final方法、static方法都是安全的,因为它们不能被重写。
在设计继承的时候Cloneable 和 Serializable接口会带来特殊的困难。如果一个类实现了这样类似的接口,那就会给继承于它的类带来很大的麻烦。所以可以使用特殊的动作来允许子类实现这些接口,而不是强制它们实现这些接口,这写细节讲述在Item 13 和 Item 86.
如果你决定在设计继承中实现Cloneable 或 Serializable接口时,应该意识到的是clone 和 readObject方法类似于构造方法,有可能直接或间接的调用被重写的方法。对于readObject而言,重写的方法会在子类反序列化之前被调用;对于clone ,重写的方法会在子类的clone方法调用之前调用。无论哪种情况都可能造成失败。例如clone时,可能破坏原始的对象以及克隆的对象。例如当复制没有完成时,被重写的方法修改了原始对象的结构。如果你觉得为设计的继承类实现Serializable接口,而且这个类重写了readResolve 或 writeReplace方法。你必须时readResolve 或 writeReplace是被保护的而不是私有的,如果这些方法是私有的,它们对子类来说是不可见的。这是另外一种为了实现继承而暴漏实现细节的情况。
现在为止可以看到,设计用于继承的类是要小心的,有很多的限制。这不是一个随随便便的决定。有很多情况下需要这么做,例如抽象类,包括接口的骨架(Item20);也有很多情况下不能这么做,例如不可变类(Item17)。
那平时我们写的类是怎样的呢?一般它们是非final、不是为继承设计的、也没有很好的文档,这样的状态都是很危险的。当改变这样的一个类时,就会破坏继承于这些类的类。这不仅是一个理论上的问题,在实际中,也经常会有相关子类异常的日志出现。
最好的解决办法是禁止在哪些没有文档和不是为继承设计的类上子类化。有两种方法禁止子类化。想对简单的是把类设计成final的。另一种是将所有构造方法都设置成私有的或者包内私有的,然后提供一个公共的静态工厂方法,这种方法能够提供了在使用子类内部的灵活性(Item17)。这两种方法都是可以接受的。
这些建议或许会有一些争议,因为很多程序员已经习惯于继承具体的类来实现一些功能,例如通知、同步、限制功能等等。如果一些类实现了包含本质的一些接口,例如Set、List或 Map,就可以通过使用包装类(Item18)的方式提供比继承更好的添加功能的方法。
如果一些类没有实现标准的接口,那么你禁止继承就可能会给一些程序员带来麻烦。如果你觉得一些类必须允许继承的话,那有一个可行的方法是保证这个类不调用可能被重写的方法并且添加文档加以说明。换句话说就是去除类自己调用自己可被重写的方法。如果这么做了,你就可以写出安全的子类,而且重写方法也不会影响任何其他方法的行为。
你可以将本来在类中调用的可重写的方法的实现新写在另一个帮助的私有方法中,然后让类调用这个私有的帮助方法。
在语法上,设计一个用于继承的类是困难的。你必须为自调用的方法写相应的文档,而且在这个类的整个生命周期都维护这个文档。因为你如果不这么做,当你修改了父类时子类就可能被破坏。要想让别人写出有效的子类,你必须暴漏几个protected的方法。如果你确认这个类不用继承,那就可以设置成final或者将构造方法设置成私有的。