Effective Java 英文版第2版閱讀筆記
阿新 • • 發佈:2018-11-17
CHAPTER 2
Item 1: Consider static factory methods instead of constructors
用靜態工廠方法(在該條目中,這個名詞與設計模式中的工廠方法模式並沒有直接的關聯)來建立物件,比起構造方法有以下好處:
- 可以任意命名。這樣,我們可以給它取一個合適的名字來清晰地表達該方法的含義,不像構造方法只能通過引數(型別、個數、型別順序等)來區分過載。
- 每次呼叫時,不一定非要建立一個新物件。這在需要頻繁地建立相等的物件,尤其是當建立的代價不菲時,可以帶來很大的效能提升。
通過靜態工廠方法在多次呼叫時返回同一個物件,這使得類對於任何時候應該存在哪些物件這一點,有著很好的控制,這樣的類也稱為例項可控的(instance-controlled)。有這樣一些原因需要寫例項可控的類:例項控制使一個類可以是單例的,或者不可例項化的;另外它也可以讓一個不可變類不存在兩個相等的例項,即a.equals(b)當且僅當a == b,如果某個類有這樣的性質,那麼它的客戶端就可以使用==操作符來代替 equals(Object) 方法,這將帶來效能提升(列舉型別就有這個性質)。 - 可以返回方法返回型別的任意子型別的物件。如Collections工具類,提供了大量便利的方法返回各種集合例項,如不可變集合,同步集合,等等,這些例項的類都是非public的,這樣使Collections框架API精減了很多,也使得調這些API的client可以只引用介面,而不是實現類。
通過傳不同的入參,同一個方法可以返回不同類的例項;不同的釋出版本之間也可能會改變返回的類與物件,這可能是為了提升軟體的可維護性或效能。 - 減少了建立泛型類的例項時的冗餘。
靜態工廠方法有如下缺點:
- 沒有public或protected構造方法的類無法被繼承。如Collections框架提供的便利實現類都無法被繼承,但也有人說這是因禍得福,因為這使得程式設計師們會去使用組合取代繼承。
- 它們不是很容易跟其它靜態方法區分開來。所以我們可能要多看看類中的靜態工廠或介面註釋,以及遵守約定俗成的靜態工廠方法的命名規範:
- valueOf——返回一個跟引數的值相同的例項,一般是型別轉換方法。
- of——valueOf的精簡版,EnumSet用得比較多。
- getInstance——根據引數返回一個例項,不一定是同一個例項。在單例類中,它沒有引數,返回的是唯一的例項。
- newInstance——跟getInstance差不多,但保證返回新的例項。
- getType——同getInstance,但是用在工廠方法在另一個類裡的情況下,"Type"表示的就是方法返回物件的型別。
- newType——同newInstance,情況同上。
總而言之,靜態工廠方法跟構造方法各有優劣,但總體來看靜態工廠方法更佳。
CHAPTER 3
Item 8: Obey the general contract when overriding equals
重寫equals方法很容易出錯,結果後果可能很嚴重哦。最簡單的辦法就是不要重寫,這樣每個例項都只與自己相等。如果以下任一條件滿足,就可以不重寫:
- 該類的每一個例項本來就是獨一無二的。如Thread類,它代表的是一種活躍的實體,而不是簡單的數值。
- 你並不關心這個類是否能提供“邏輯上相等”的測試。如Random類,判斷兩個Random例項是否能提供相同的隨機序列,這個可以有,但是一般不需要。
- 父類已經重寫了equals,它的行為也正適合這個子類。比如,很多Set的實現類從AbstractSet繼承了equals方法,List實現類繼承了AbstractList的,Map實現類繼續了AbstractMap的。
- 這個類是private或者package-private的,並且你確定它的equals方法絕對不會被呼叫。有爭議地,有人說這種情況下應該重寫equals方法以防它被意外呼叫:
@Override
public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
所以,當一個類有“邏輯相等”的概念(而不僅僅是物件的身份),且它沒有父類來提供合適的equals時,重寫equals就是比較合適的。值型別常常是這種情況。重寫equals不僅僅是讓類滿足我們對其邏輯相等的預期,更是為了讓例項在作為map的key和set的元素時,表現出可預測的、正確的行為。
重寫equals方法要滿足以下約定:
- 自反
- 對稱
- 傳遞
- 一致
- (取個什麼名呢)對於任何非空引用x,x.equals(null)必須返回false。
有一點需要注意,當你擴充套件一個可例項化的類,並且新增值成員,那麼你是沒辦法遵循上述所有規則的。但是可以用組合代替繼承來解決這個問題。
不要讓equals方法依賴不可靠的資源。例如,java.net.URL的equals方法依賴於URL物件相關聯的host的IP地址,這在實踐中會導致問題,但因為相容性的問題,這個問題沒法修復。幾乎沒有例外地,equals方法應該對記憶體中的物件表現出確定的行為。
另外,重寫equals(Object o)時沒必要對o進行判空,因為o instanceof SomeType在o為null時會返回false。
綜上,做到以下幾點可以寫出一個高質量的equals方法:
- 用==來檢查是否引數就是該物件的引用,如果是,返回true。這僅僅是效能上的優化,但考慮到做比較可能開銷不低,這一點還是值得的。
- 用instanceof來檢查引數是否具有正確的型別,如果否,返回false。典型情況下,正確的型別就是這個equals方法的類; 有時候,正確的型別是該類所實現的某個介面,舉例來說,集合介面如Set, List, Map, Map.Entry都具有這個性質。
- 將引數轉化為正確的型別。
- 對於該類中每一個“重要的”的成員,檢查引數的該欄位與這個物件的該欄位是否匹配,如果所有的都能匹配,返回true,否則返回false。
對於非float和double的原始型別成員,用==來比較;對於物件成員,遞迴地呼叫equals來比較;對於float成員,用Float.compare方法;對於double成員,用Double.compare方法。對於陣列成員,將上述準則應用於每一個元素,如果所有元素都需要比較,可以使用Arrays.equals的某一版本(1.5引入)。
考慮到物件成員可能是null,可使用下述表示式來比較,以避免NPE:
(field == null ? o.field == null : field.equals(o.field))
又因為field和o.field經常是identical的(應該是指同一個,而不是邏輯相等吧),下述表示式可能效能更好:
(field == o.field || (field != null && field.equals(o.field)))
equals方法的效能也可能會受成員比較的順序影響。應該先比較那些更有可能不同的成員,或者是那些比較開銷較小的成員。一定不能比較與物件邏輯狀態無關的成員,如用來同步操作的Lock成員。不需要去比較冗餘成員,即可以根據“重要成員”計算出來的成員,不過這樣做也有可能提升效能,即當某個冗餘成員表示了該類的一個總體情況,那麼該成員如果不相等,可以節省比較具體資料的成本。如一個多邊形類,如果你儲存了它的面積,那麼當兩個多邊形面積不等時,你就不再需要去比較它的邊和角了。 - 寫完一個equals方法時,要考慮它是否滿足對稱性、傳遞性、一致性。不要僅僅思考,還要寫UT來檢查。
具體的符合上述規則的equals案例可以參見Item 9。
下面是最後幾個注意事項:
- 重寫equals時,一定要重寫hashCode。
- 不要自作聰明,簡單地測試成員是否相等就好。
- equals的引數型別就用Object,不要用任何其它型別去替代。否則你寫的equals方法並沒有覆蓋Object.equals,而是重寫了它。雖然可以在正常的版本之外再附加一個這樣的強型別的equals方法(只要兩個方法返回結果相同),但是它帶來的效能優化很少,卻增加了複雜性(Item 55)。
用@Override註解就可以避免上述情況。