2011年12月26日 星期一

《Effective Java》筆記

一. 創建和銷毀物件

1.以 static factory methods 取代建構式。它和建構式不同,它有自己的名稱、不需要每次被呼叫時都創建一個物件、可以回傳一個「隸屬於函式回返值型別之任何子型別(subtype)」的物件,其缺點為:若 classes 沒有了 public 或 protected 建構式,將無法被 subclassed、它們無法明顯地和其他 static 函式有所區分

2.以 private 建構式厲行 singleton(單件,是指只能被實體化(instantiated)一次的 class)性質、不可實體化性質

3.手動將逾期的 object references 設為 null。任何時候只要 class 管理了它自己的記憶體,你都該保持警惕

4.避免使用 finalizers。Java不僅不保證 finalizers 及時被執行,也不保證它們終將被執行




二. 通用於所有物件的函式

1.你不應覆寫 equals() 的情況:
(1)class 的每個實體具有本質唯一性(ingerently unique)。代表「行為實物」(active entities)的 classes 就是如此,例如 Thread
(2)你不在意 class 是否提供邏輯相等性
(3)某個 superclass 已覆寫 equals(),而從該 superclass 繼承而來的行為又已適任
(4)你的 class 是 private 或 package-private,而你確定其 equals() 永遠不會被呼叫

2.你應覆寫 equals() 的情況:若 class 具有邏輯相等性,而不僅僅只看物件是否一致,而且其 superclass 尚未覆寫 equals() 實現出它所期望的行為(這樣的 classes 通常吃為 value classes),你想藉由 equals() 來比較 reference to value object 時,希望知道的是它們是否邏輯相等,並不計較它們是否指向同一個物件

3.撰寫高品質 equals() 的要點:
(1)以 == 運算子檢查「引數是否為物件自身的 reference」。如果是,便傳回 true
(2)以 instanceof 運算子檢查引數是否為正確型別。如果不是,就傳回 false。通常「正確型別」就是「equals() 函式所屬型別」
(3)將引數轉換為正確型別
(4)對於 class 內每一個有意義的欄位(fields),檢驗引數中的該欄位是否與物件內的對應欄位吻合。如果所有測試都成功,就傳回 true;否則傳回 false
(5)equals() 會受到欄位比較次序的影響。你應先比較那些「最可能不一致的」或「開銷最小的」欄位,兩者兼具更好。贅餘欄位(亦即「可從其他有意義的欄位計算而得者」),則不一定要比較,但若某個贅餘欄位相當於整個物件的概括描繪,那麼比較這個欄位就能提高效率。此外,千萬不要將不屬於物件邏輯狀態的欄位也納入比較
(6)不要寫出依賴「不可依賴資源(unreliable resource)」的 equals()
(7)不要在 equals() 宣告式中以其他型別代替 Object
(8)當你完成 equals() 的撰寫,反問自己三個問題:
<1>它有對稱性(Symmetry,兩物件必須對於彼此是否相等取得一致)嗎?
<2>它有遞移性(Transitivity,若物件 A 等於物件 B,而物件 B 又等於物件 C,則物件 A 必等於物件 C)嗎?
<3>它有一致性(Consistency,若兩個物件相等,除非其中一個或兩個被改動,否則它們應該始終保持相等)嗎?

4.hashCode() 應該和 equals() 一同被覆寫。撰寫高品質 hash 函式的要點:
(1)將一個非 0 常數儲存在 int result 變數中
(2)對物件中的每一個有意義的欄位 f(被 equals() 所考慮的每一個欄位)進行下列處理:
A.對這個欄位計算出型別為 int 的 hash 碼 c :
<1>若欄位是個 bollean,計算 (f ? 0 : 1)
<2>若欄位是個 byte, char, short 或 int,計算 (int)f
<3>若欄位是個 long,計算 (int)(f^(f >>> 32))
<4>若欄位是個 float,計算 Float.floatToIntBits(f)
<5>若欄位是個 double,計算 Double.doubleToLongBits(f),然後將其計算結果按步驟 <3> 處理
<6>若欄位是個 object reference,而且 class 的 equals() 透過「遞迴呼叫 equals()」的方式來比較這一欄位,那麼就同樣也對該欄位遞迴呼叫 hashCode()。如果需要更複雜的比較,就對該欄位運算一個標準表述式(canonical representation),並對該標準表述式呼叫 hashCode()。若欄位值為 null,就傳回 0
<7>若欄位是個 array,就將每個元素視為獨立欄位。也就是說對每一個有意義的元素施行上述規則,用以計算出 hash 碼,然後再依步驟(2).B 將這些數值組合起來
B.將步驟 A 計算出來的 hash 碼 c,按下列公式組合到變數 result 中:
result = 37*result + c;
(3)傳回 result
(4)完成 hashCode() 之後,反問自己:是否相等的實體具有相等的 hash 碼?

5.所有 subclasses 都應覆寫 toString()。實現 toString() 時,必須決定是否在文件中明確說明回返值的格式,你最好對 value classes(如電話號碼或矩陣)這麼做。一旦你指定了格式,應再提供一個相應的 String 建構式(或一個 static factory)
而「明確說明回返值的格式」的缺點在於,一旦你這麼做,而且你的 class 被廣泛使用的話,便將永遠被這種格式纏住而無法擺脫。只要不明確指定格式,你就保留了在後續版本中「增加資訊」或「改善格式」的靈活性

6.若你在撰寫一個具有明顯內在次序(natural,如字母順序、數字順序或時間順序)的 value class,應使用 compareTo() 取代 equals(),其差異在於你無須在型別轉換之前檢查引數型別。此外,「欄位比較」本身是「順序比較」,而非「相等比較」




三. 類別和介面

1.你應該使每一個 class 或其成員盡可能不被外界存取。換句話說,你應將 classes 和其他成員的可存取性(accessibility)最小化。設計良好的模組會隱藏其所有實作細目,把 API 和實作細節乾淨地分開來;模組和模組之間只透過 APIs 進行通訊,不需知道對方內部工作細節。這個概念稱為資訊隱藏(information hiding)或封裝(encapsulation)

2.public classes 不應該擁有 public 欄位,但有個例外:public classes 可透過 public static final 欄位來表現常數,而這些欄位應該只包含基本型數值,或包含「指向不可變(immutable)物件」的 reference

3.盡量使用不可變類別(immutable classes),而非可變類別(mutable classes)。當你在製造一個不可變類別時,應遵循底下 5 個原則:
(1)不要提供任何「可修改物件內容」的函式
(2)保證沒有任何函式可被覆寫
(3)令所有欄位為 final
(4)令所有欄位為 private
(5)確保對任何可變組件(mutable components)的互斥存取(exclusive access)。如果你的 class 中有任何欄位指向可變物件,你必須確保其客戶不能獲得這些物件的 references。你可以在建構式、存取式(accessor)和 readObject() 中使用保護性拷貝(defensive copy)

4.immutable objects 唯一的缺點是,每個不同的值需要一個獨立物件。因此當你在面對大型物件時,必須付出不小的成本

5.優先考慮複合(composition),然後才是繼承(inheritance)。只有當 subclass 確實是 superclass 的一個子型別(subtype)時,才適合使用繼承

6.除非專為繼承而設計並提供完善的文件,否則不要使用繼承。文件中必須指出 class 在什麼環境(情況)下可以呼叫一個可被覆寫函式,而其描述文字應以「本實作碼」(This implementation)起頭,暗示此一描述與內部細節有關
此外,class 還必須遵守一些限制規定,才能被用於繼承機制:無論直接或間接,建構式絕對不能呼叫可被覆寫函式(overridable methods)

7.要消除 class 對於其可被覆寫函式的自用性(self-use),而且不改變 class 的行為,你可以把可被覆寫函式的「函式本體」搬移到一個 private 輔助函式中,並讓每一個可被覆寫函式呼叫其 private 輔助函式,然後直接喚起可被覆寫函式的 private 輔助函式,用以代替可被覆寫函式的每一個自用動作

8.盡量以 interfaces 取代 abstract classes。要實現 abstract class 所定義的型別,你必須先做出該 abstract class 的一個 subclass;而任何 class 只要定義所有必要函式,並遵守一般契約,都可以實現一個 interface,不論該 class 位於 class 繼承體系的何處
而兩者另一個差異,在於 abstract classes 可以內含函式實作碼,interface 則完全不允許

9.骨幹實作類別(AbstractInterface)可以提供 abstract classes 的實作支援,卻不必接受 abstract classes 被當作型別定義實需要接受的嚴格限制。要撰寫骨幹實作類別時,首先你必須分析 interface,確定哪些函式是其他函式賴以生存的基本元素。這些基本元素應該成為你的「骨幹實作類別」中的 abstract methods。然後你必須為 interface 內的所有其他函式提供具象實作(concrete implementations)

10.interface 通常是定義「允許多份實作同時存在」的型別的最佳辦法。而當你認為「演進的容易性」比「程式的彈性和功能」更重要時,則應使用一個 abstract class 來定義型別
如果你匯出一個有所作為的(nontrivial)interface,你則應為它提供一個「骨幹實作類別」 

11.interfaces 指應被用來定義型別(types)。如果需要匯出常數,而常數與既有的 class 或 interface 緊密關聯,你就應該把它們加入那個 class 或 interface 中。如果常數適合作為列舉型(enumerated type)成員,你應該以一個 typesafe enum class 匯出它們,否則應該以一個「不可被實體化」的 utility class 加以匯出

12.優先考慮 static member classes,然後才是 nonstatic。如果 nested class 需要在某函式之外可被看見,或如果它太長而不是和塞入一個函式內,應使用 member class。而如果 member class 需要一個 reference 指向其外圍實體,則讓它成為 nonstatic;否則應讓它成為 static
假設 class 存在於一個函式內,如果你只需在唯一一個位置上創建實體,並且有一個既存的型別可以描繪出該 class 的特徵,應把它做成一個 annoymous class,否則做成一個 local class




四. 函式

1.每當你撰寫一個函式或建構式,應該思考其參數需要什麼樣的限制。你應把這些限制明白記載於文件之中,並在函式起始處對它們做明確的有效性(validity)檢驗。除非有效性檢驗的成本過高或不切實際,而且計算過程中會暗自進行有效檢驗

2.必要時製作出「保護性拷貝」(defensive copies)。你應盡可能使用 immutable objects 作為你的物件的組件,使你不再需要操心保護性拷貝

3.永遠不要匯出兩個參數個數相同的重載(overloading)函式。如果你是為了翻新一個舊的 class 以求實現一個新的 interface,你應該確保所有「收到相同引數」的重載函式表現出相同的行為

4.寧願傳回長度為 0 的 arrray,也不要傳回 null

5.為了恰當而正確地將你的 API 文件化,你必須在每一個被匯出的 class、interface、建構式、函式、欄位的宣告式前加上相應的文件註釋(doc comment)。每一個函式的 doc comment 應該簡潔扼要地描述函式與客戶之間的契約。這些契約應該說明函式「做了些什麼」而不是「函式如何做」。而針對繼承而設計的 class 則不在此限
doc comment 應該列舉函式的所有 precondition(前提),也就是客戶為求成功呼叫該函式需要滿足哪些條件;也應該列舉出 postcondition(後期狀態),也就是函式成功結束後,呼叫端會得到什麼結果。除了 precondition 和 postcondition,函式還應該將所有 side effect(連帶影響、副作用)文件化。所謂的 side effect 是系統狀態狀態上的一種可察變化,它並不是達到 postcondition 所必須的




五. 一般編程原則

1.將區域變數的作用域(scope)最小化,最有效的技巧就是在變數第一次被使用時才宣告它

2.如需精確運算結果,請勿使用 float 和 double。當數額不超過 9 個小數位,可以使用 int;若不超過 18 個小數位,則使用 long;一旦超過了 18 位小數,就使用 bigDecimal

3.如果你有更合適的資料型別(已經存在或可被設計),應避免把物件表示為字串。常常被錯誤地替換為 String 的型別包括基本資料型別(primitive types)、列舉型別(enumerated types)和聚合型別(aggregate types)

4.字串接合(string concatenation)不應使用於規模較大的情況,你可以採用 StringBuffer 的函式替代之、使用字元 array 或每次只處理一個字串,但不要將它們組合起來

5.盡量使用 interfaces 而非 classes 來指涉(指向、引用, refer to)物件。你應養成「以 interface 為型別」的習慣。唯一真正需要 class 的時機是你創建物件的時刻

6.優先使用 interfaces,然後才考慮 reflection。如果你撰寫的程式必須和編譯期間未知的某些 class 合作,你應該只在「將物件實體化」時使用 reflection,然後就改以編譯期間已知的 interface 或 superclass 來取用該物件

7.盡可能少用 native 函式

8.一旦你謹慎地設計了程式,並產生清楚的、一致化的、結構良好的實作品之後,如果還是不滿意程式效率,才應考慮將它最佳化。最佳化的第一個對象是檢查你所選用的演算法。當你最佳化後,應記得量測效率




六. 異常

1.異常機制(exceptions)應該僅僅被用於意外情勢中,永遠不要把它們當作一般的流程控制手段。
如果「被存取物」被並行(concurrently)處理而沒有外部同步化(external synchronization),或如果它可被外部因素誘發「狀態改變」,那麼使用「特異回傳值」可能是必須的;而如果「狀態測試函式」會重複「狀態相依函式」的工作,那在十分要求效率的情況下,就得使用「傳回特異值」的函式。至於其他所有情況,可以說是「狀態測試」優於「傳回特異值」的函式

2.對可復原狀態(recoverable conditions)使用可控式異常(checked exceptions);對編程錯誤使用執行期異常(runtime exceptions)

3.不要濫用可控式異常,這會使得 API 用起來很不方便,它們會強制程式員處理異常狀態

4.盡量使用標準異常(standard exceptions)

5.拋出與其抽象層(abstraction)相應的異常

6.對每一個函式所拋出的異常詳加說明

7.以詳細的訊息記錄「失敗情況下捕獲的資訊」(failure-capture information)

8.保持 failure atomicity(失敗之極微性)。最簡單的辦法就是設計不可變物件。如果函式所操作的是可變物件,你可以在執行操作之前先檢查參數的有效性,使得物件被改動之前會先行引發適當異常




七. 執行緒

1.任何時候只要多個執行緒共享可變資料,每一個讀寫資料的執行緒都必須先獲得一個鎖件(lock)。不要因為「atomic 讀寫承諾」而斷了你的「執行正確同步控制」的念頭

2.為了避免造成 deadlock,在一個 synchronized 函式或 synchronized 區段內,絕對不要將控制權讓渡給客戶。換句話說,不要在同步區域內呼叫一個「設計目的主要是為了被覆寫」的 public 函式或 protected 函式(這樣的函式通常是抽象的,但有時它們有具體的預設實作碼)

3.通常,你應該在 synchronized 區域內盡可能少做工作。取得鎖件、檢查共享資料、對資料進行必要轉換,然後放棄鎖件。如果你必須執行某些耗時活動,應該想辦法把這樣的活動從 synchronized 區域裡移出來

4.如果你正在撰寫一個低階抽象層,它通常被單執行緒使用,或做為更大同步物件內的組件,你就應該考慮限制 class 的內部同步

5.如果你所撰寫的 class 會在「需要 synchronization」的環境中和「不需要 synchronization」的環境中被大量使用,那麼你應同時提供同步的(多緒安全的)和非同步的(執行緒相容的)兩種變體。實現辦法是提供一個 wrapper class(外覆類別),實作出「用以描述此 class」的一個 interface,並在「對 wrapper class 的相應函式執行轉呼叫(forwarding method invocations)」之前,先執行適當的同步化控制

6.絕對不要在迴圈之外喚起 wait()。通常你應該優先考慮使用 notifyAll(),然後才是 notify();然而前者會引發實質性的效率損耗,而如果使用 notify(),則必須特別小心以保證程式的活躍度(liveness)

7.不要依賴執行緒排程器(thread scheduler),也不要倚賴 Thread.Yield() 或執行緒優先權,記得永遠不要試圖以它們來修正無法正常運作的程式。最好的辦法是確認任何時刻都只有少量可運行(runnable)的執行緒。你應只讓每個執行緒做少量工作,然後以 Object.wait() 等待某些條件(狀態)發生,或以 Thread.sleep() 休眠一段指定時間。執行緒不應該「等待卻忙碌」(busy-wait),也就是說它不應該為了等待某些條件的出現,而不斷地反覆檢查某個資料結構

8.以文件說明你的「多緒安全性」,當你要進行文件化時,你必須指明:
(1)哪個呼叫序列要求外部同步設施
(2)為排除並行存取,哪個(些)鎖件是必須的。通常需要的是作用於 class 實體本身的鎖件,但也有例外: 如果一個物件表示出其他某些物件的映件(view),那麼客戶必須取得作用於後備物件(backing object)身上的鎖,以免對後備物件進行直接修改

9.避免使用執行緒群組(thread groups)




八. 序列化

1.審慎實現 Serializable,除非一個 class 在被使用一小段時間後就要被丟棄。面對「為繼承而設計」的 class 更需要額外小心。面對這樣的 classes,其 subclasses 究竟可實現 Serializable 或被禁止 Serializable,兩者的分水嶺在於該 class 是否提供了一個「可取用的無參數建構式」(accessible parameterless constructor)。如果有,就允許(但不強制)subclasses 也實現 Serializable

2.無論你選擇什麼樣的 serialized form,應在你所寫的每一個 serializable class 上宣告一個明確的 serial version UID 欄位

3.當你認定某個 class 應該成為 serializable,便應思考使用什麼樣的 serialized form。只有當「預設之 serialized form 是物件邏輯狀態的合理描述」時,才應該使用預設的 serialized form,否則應設計一個自定的 serialized form,用以適切描述物件

4.任何時候當你寫下一個 readObject()時,想像你正在寫下一個 public 建構式,無論收到什麼樣的 byte stream,它都必須生產出一個有效實體。不要假設那個 byte stream 一定代表一個「真正被 serialized 的實體」,因為有可能會有偽造的。
撰寫 readObject() 的準則:
(1)如果 class 帶有 object reference 欄位,而且後者必須保持為 private,請對每一個被儲存於如此欄位的物件進行保護性拷貝
(2)面對帶有約束條件(invariants)的 class,應檢驗其約束條件並於檢驗失敗時拋出 InvalidObjectException 異常。檢驗動作應該緊跟在任何保護性拷貝之後
(3)如果整個物件藍圖(object graph)在被 deserialized 之後必須確認有效,那就應該使用 ObjectInputVlaidation interface
(4)無論直接或間接,不要再 readObject() 中喚起 class 內可被覆寫的任何函式

5.你必須使用 readResolve() 來保護 singletons 或其他「對實體個數有所管控的」(instance-controlled)classes 的「實體個數約束條件」。對於「package 之外禁止繼承」的 classes 而言,readResolve() 也可被用來做為保護性 readObject()的一個簡單替代方案

沒有留言:

張貼留言