Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。java
条目 18中提醒你注意继承没有设计和文档说明的“外来”类的子类化的危险。 那么为了继承而设计和文档说明一个类是什么意思呢?程序员
首先,这个类必须准确地描述重写这个方法带来的影响。 换句话说,该类必须文档说明可重写方法的自用性(self-use)。 对于每一个公共或受保护的方法,文档必须指明方法调用哪些重写方法,以何种顺序以及每次调用的结果如何影响后续处理。 (重写方法,这里是指非final修饰的方法,不管是公开仍是保护的。)更通常地说,一个类必须文档说明任何可能调用可重写方法的状况。 例如,后台线程或者静态初始化代码块可能会调用这样的方法。api
调用可重写方法的方法在文档注释结束时包含对这些调用的描述。 这些描述在规范中特定部分,标记为“Implementation Requirements,”,由Javadoc标签@implSpec
生成。 本节介绍该方法的内部工做原理。 下面是从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 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 remove method and this collection contains the specified object.
从该集合中删除指定元素的单个实例(若是存在,optional
实例操做)。 更正式地说,若是这个集合包含一个或多个这样的元素,删除使得Objects.equals(o, e)
的一个元素e。 若是此集合包含指定的元素(或者等同于此集合因调用而发生了更改),则返回true。ide
实现要求:这个实现迭代遍历集合查找指定元素。 若是找到元素,则使用迭代器的remove
方法从集合中删除元素。 请注意,若是此集合的iterator
方法返回的迭代器未实现remove
方法,而且此集合包含指定的对象,则此实现将引起UnsupportedOperationException
异常。工具
这个文档毫无疑问地说明,重写iterator
方法会影响remove
方法的行为。 它还描述了iterator
方法返回的Iterator行为将如何影响remove
方法的行为。 与条目 18中的状况相反,在这种状况下,程序员继承HashSet
并不能说明重写add方法是否会影响addAll方法的行为。性能
可是,这是否违背了一个良好的API文档应该描述给定的方法是什么,而不是它是如何作的呢? 是的,它确实!这是继承违反封装这一事实的不幸后果。要文档说明一个类以即可以安全地进行子类化,必须描述清楚那些没有详细说明的实现细节。学习
@implSpec
标签是在Java 8中添加的,而且在Java 9中被大量使用。这个标签应该默认启用,可是从Java 9开始,除非经过命令行开关-tag "apiNote:a:API Note:”
,不然Javadoc实用工具仍然会忽略它。测试
设计继承涉及的不只仅是文档说明自用的模式。 为了让程序员可以写出有效的子类而不会带来不适当的痛苦,一个类可能以明智选择的受保护方法的形式提供内部工做,或者在罕见的状况下,提供受保护的属性。 例如,考虑java.util.AbstractList
中的removeRange
方法:ui
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. Implementation Requirements: This implementation gets a list iterator positioned before fromIndex and repeatedly calls ListIterator.nextfollowed 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.
今后列表中删除索引介于fromIndex
(包含)和inclusive
(不含)之间的全部元素。 将任何后续元素向左移(减小索引)。 这个调用经过(toIndex - fromIndex)
元素来缩短列表。 (若是toIndex == fromIndex
,则此操做无效。)
这个方法是经过列表及其子类的clear操做来调用的。重写这个方法利用列表内部实现的优点,能够大大提升列表和子类的clear操做性能。
实现要求:这个实现获取一个列表迭代器,它位于fromIndex
以前,并重复调用ListIterator.remove
和ListIterator.next
方法,直到整个范围被删除。 注意:若是ListIterator.remove
须要线性时间,则此实现须要平方级时间。
参数:
fromIndex 要移除的第一个元素的索引
toIndex 要移除的最后一个元素以后的索引
这个方法对List实现的最终用户来讲是没有意义的。 它仅仅是为了使子类很容易提供一个快速clear方法。 在没有removeRange
方法的状况下,当在子列表上调用clear方法,子类将不得不使用平方级的时间,不然,或从头重写整个subList机制——这不是一件容易的事情!
那么当你设计一个继承类的时候,你如何决定暴露哪些的受保护的成员呢? 不幸的是,没有灵丹妙药。 所能作的最好的就是努力思考,作出最好的测试,而后经过编写子类来进行测试。 应该尽量少地暴露受保护的成员,由于每一个成员都表示对实现细节的承诺。 另外一方面,你不能暴露太少,由于失去了保护的成员会致使一个类几乎不能用于继承。
测试为继承而设计的类的惟一方法是编写子类。 若是你忽略了一个关键的受保护的成员,试图编写一个子类将会使得遗漏痛苦地变得明显。 相反,若是编写的几个子类,并且没有一个使用受保护的成员,那么应该将其设为私有。 经验代表,三个子类一般足以测试一个可继承的类。 这些子类应该由父类做者之外的人编写。
当你为继承设计一个可能被普遍使用的类的时候,要意识到你永远承诺你文档说明的自用模式以及隐含在其保护的方法和属性中的实现决定。 这些承诺可能会使后续版本中改善类的性能或功能变得困难或不可能。 所以,在发布它以前,你必须经过编写子类来测试你的类。
另外,请注意,继承所需的特殊文档混乱了正常的文档,这是为建立类的实例并在其上调用方法的程序员设计的。 在撰写本文时,几乎没有工具将普通的API文档从和仅仅针对子类实现的信息,分离出来。
还有一些类必须遵照容许继承的限制。 构造方法毫不能直接或间接调用可重写的方法。 若是违反这个规则,将致使程序失败。 父类构造方法在子类构造方法以前运行,因此在子类构造方法运行以前,子类中的重写方法被调用。 若是重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行。 为了具体说明,这是一个违反这个规则的类:
public class Super { // Broken - constructor invokes an overridable method public Super() { overrideMe(); } public void overrideMe() { } }
如下是一个重写overrideMe
方法的子类,Super类的惟一构造方法会错误地调用它:
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,由于在Sub构造方法有机会初始化instant
属性以前,overrideMe
被Super构造方法调用。 请注意,这个程序观察两个不一样状态的final属性! 还要注意的是,若是overrideMe
方法调用了instant
实例中任何方法,那么当父类构造方法调用overrideMe
时,它将抛出一个NullPointerException
异常。 这个程序不会抛出NullPointerException
的惟一缘由是println
方法容忍null参数。
请注意,从构造方法中调用私有方法,其中任何一个方法都不可重写的,那么final方法和静态方法是安全的。
Cloneable
和Serializable
接口在设计继承时会带来特殊的困难。 对于为继承而设计的类来讲,实现这些接口一般不是一个好主意,由于这会给继承类的程序员带来很大的负担。 然而,能够采起特殊的行动来容许子类实现这些接口,而不须要强制这样作。 这些操做在条目 13和条目 86中有描述。
若是你决定在为继承而设计的类中实现Cloneable
或Serializable
接口,那么应该知道,因为clone
和readObjec
t方法与构造方法类似,因此也有相似的限制:clone和readObject都不会直接或间接调用可重写的方法。在readObject
的状况下,重写方法将在子类的状态被反序列化以前运行。 在clone
的状况下,重写方法将在子类的clone
方法有机会修复克隆的状态以前运行。 在任何一种状况下,均可能会出现程序故障。 在clone
的状况下,故障可能会损坏原始对象以及被克隆对象自己。 例如,若是重写方法假定它正在修改对象的深层结构的拷贝,可是还没有建立拷贝,则可能发生这种状况。
最后,若是你决定在为继承设计的类中实现Serializable
接口,而且该类有一个readResolve
或writeReplace
方法,则必须使readResolve
或writeReplace
方法设置为受保护而不是私有。 若是这些方法是私有的,它们将被子类无声地忽略。 这是另外一种状况,把实现细节成为类的API的一部分,以容许继承。
到目前为止,设计一个继承类须要很大的努力,而且对这个类有很大的限制。 这不是一个轻率的决定。 有些状况显然是正确的,好比抽象类,包括接口的骨架实现(skeletal implementations)(条目 20)。 还有其余的状况显然是错误的,好比不可变的类(条目 17)。
可是普通的具体类呢? 传统上,它们既不是final的,也不是为了子类化而设计和文档说明的,可是这种状况是危险的。每次修改这样的类,则继承此类的子类将被破坏。 这不只仅是一个理论问题。 在修改非final的具体类的内部以后,接收与子类相关的错误报告并很多见,这些类没有为继承而设计和文档说明。
解决这个问题的最好办法是,在没有想要安全地子类化的设计和文档说明的类中禁止子类化。 有两种方法禁止子类化。 二者中较容易的是声明类为final。 另外一种方法是使全部的构造方法都是私有的或包级私有的,而且添加公共静态工厂来代替构造方法。 这个方案在内部提供了使用子类的灵活性,在条目 17中讨论过。两种方法都是能够接受的。
这个建议可能有些争议,由于许多程序员已经习惯于继承普通的具体类来增长功能,例如通知和同步等功能,或限制原有类的功能。 若是一个类实现了捕获其本质的一些接口,好比Set,List或Map,那么不该该为了禁止子类化而感到愧疚。 在条目 18中描述的包装类模式为加强功能提供了继承的优越选择。
若是一个具体的类没有实现一个标准的接口,那么你可能会经过禁止继承来给一些程序员带来不便。 若是你以为你必须容许从这样的类继承,一个合理的方法是确保类从不调用任何可重写的方法,并文档说明这个事实。 换句话说,彻底消除类的自用(self-use)的可重写的方法。 这样作,你将建立一个合理安全的子类。 重写一个方法不会影响任何其余方法的行为。
你能够机械地消除类的自我使用的重写方法,而不会改变其行为。 将每一个可重写的方法的主体移动到一个私有的“帮助器方法”,并让每一个可重写的方法调用其私有的帮助器方法。 而后用直接调用可重写方法的专用帮助器方法来替换每一个自用的可重写方法。
你能够机械地消除类的自用的重写方法,而不会改变其行为。 将每一个可重写的方法的主体移到一个私有的“辅助方法(helper method)”,并让每一个可重写的方法调用其私有的辅助方法。 而后用直接调用可重写方法的专用辅助方法来替换每一个自用的可重写方法。
总之,设计一个继承类是一件很辛苦的事情。 你必须文档说明全部的自用模式,一旦你文档说明了它们,必须承诺为他们的整个生命周期。 若是你不这样作,子类可能会依赖于父类的实现细节,而且若是父类的实现发生改变,子类可能会损坏。 为了容许其余人编写高效的子类,可能还须要导出一个或多个受保护的方法。 除非你知道有一个真正的子类须要,不然你可能最好是经过声明你的类为final禁止继承,或者确保没有可访问的构造方法。