Effective Java 第三版——19. 若是使用繼承則設計,並文檔說明,不然不應使用

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java

Effective Java, Third Edition

19. 若是使用繼承則設計,並文檔說明,不然不應使用

條目 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.removeListIterator.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方法和靜態方法是安全的。

CloneableSerializable接口在設計繼承時會帶來特殊的困難。 對於爲繼承而設計的類來講,實現這些接口一般不是一個好主意,由於這會給繼承類的程序員帶來很大的負擔。 然而,能夠採起特殊的行動來容許子類實現這些接口,而不須要強制這樣作。 這些操做在條目 13和條目 86中有描述。

若是你決定在爲繼承而設計的類中實現CloneableSerializable接口,那麼應該知道,因爲clonereadObject方法與構造方法類似,因此也有相似的限制:clone和readObject都不會直接或間接調用可重寫的方法。在readObject的狀況下,重寫方法將在子類的狀態被反序列化以前運行。 在clone的狀況下,重寫方法將在子類的clone方法有機會修復克隆的狀態以前運行。 在任何一種狀況下,均可能會出現程序故障。 在clone的狀況下,故障可能會損壞原始對象以及被克隆對象自己。 例如,若是重寫方法假定它正在修改對象的深層結構的拷貝,可是還沒有建立拷貝,則可能發生這種狀況。

最後,若是你決定在爲繼承設計的類中實現Serializable接口,而且該類有一個readResolvewriteReplace方法,則必須使readResolvewriteReplace方法設置爲受保護而不是私有。 若是這些方法是私有的,它們將被子類無聲地忽略。 這是另外一種狀況,把實現細節成爲類的API的一部分,以容許繼承。

到目前爲止,設計一個繼承類須要很大的努力,而且對這個類有很大的限制。 這不是一個輕率的決定。 有些狀況顯然是正確的,好比抽象類,包括接口的骨架實現(skeletal implementations)(條目 20)。 還有其餘的狀況顯然是錯誤的,好比不可變的類(條目 17)。

可是普通的具體類呢? 傳統上,它們既不是final的,也不是爲了子類化而設計和文檔說明的,可是這種狀況是危險的。每次修改這樣的類,則繼承此類的子類將被破壞。 這不只僅是一個理論問題。 在修改非final的具體類的內部以後,接收與子類相關的錯誤報告並很多見,這些類沒有爲繼承而設計和文檔說明。

解決這個問題的最好辦法是,在沒有想要安全地子類化的設計和文檔說明的類中禁止子類化。 有兩種方法禁止子類化。 二者中較容易的是聲明類爲final。 另外一種方法是使全部的構造方法都是私有的或包級私有的,而且添加公共靜態工廠來代替構造方法。 這個方案在內部提供了使用子類的靈活性,在條目 17中討論過。兩種方法都是能夠接受的。

這個建議可能有些爭議,由於許多程序員已經習慣於繼承普通的具體類來增長功能,例如通知和同步等功能,或限制原有類的功能。 若是一個類實現了捕獲其本質的一些接口,好比Set,List或Map,那麼不該該爲了禁止子類化而感到愧疚。 在條目 18中描述的包裝類模式爲加強功能提供了繼承的優越選擇。

若是一個具體的類沒有實現一個標準的接口,那麼你可能會經過禁止繼承來給一些程序員帶來不便。 若是你以爲你必須容許從這樣的類繼承,一個合理的方法是確保類從不調用任何可重寫的方法,並文檔說明這個事實。 換句話說,徹底消除類的自用(self-use)的可重寫的方法。 這樣作,你將建立一個合理安全的子類。 重寫一個方法不會影響任何其餘方法的行爲。

你能夠機械地消除類的自我使用的重寫方法,而不會改變其行爲。 將每一個可重寫的方法的主體移動到一個私有的「幫助器方法」,並讓每一個可重寫的方法調用其私有的幫助器方法。 而後用直接調用可重寫方法的專用幫助器方法來替換每一個自用的可重寫方法。

你能夠機械地消除類的自用的重寫方法,而不會改變其行爲。 將每一個可重寫的方法的主體移到一個私有的「輔助方法(helper method)」,並讓每一個可重寫的方法調用其私有的輔助方法。 而後用直接調用可重寫方法的專用輔助方法來替換每一個自用的可重寫方法。

總之,設計一個繼承類是一件很辛苦的事情。 你必須文檔說明全部的自用模式,一旦你文檔說明了它們,必須承諾爲他們的整個生命週期。 若是你不這樣作,子類可能會依賴於父類的實現細節,而且若是父類的實現發生改變,子類可能會損壞。 爲了容許其餘人編寫高效的子類,可能還須要導出一個或多個受保護的方法。 除非你知道有一個真正的子類須要,不然你可能最好是經過聲明你的類爲final禁止繼承,或者確保沒有可訪問的構造方法。

相關文章
相關標籤/搜索