Effective Java 之個人總結

創建和銷燬對象

1、靜態工廠方法代替構造器

  • 靜態工廠方法有名稱,能確切地描述正被返回的對象。
  • 不必每次調用都創建一個新的對象。
  • 可以返回原返回類型的任何子類對象。
  • 創建參數化類型實例時更加簡潔,比如調用構造 HashMap 時,使用 Map<String,List<String> m = HashMap.newInstance(),與Map<String,List<String>m> = new HashMap<String,List<String>>();。

2、遇到多個構造器參數時要考慮用構建器

  • 靜態工廠和構造器不能很好地擴展到大量的可選參數。
  • JavaBean 模式下使用 setter 來設置各個參數,無法僅通過檢驗構造器參數的有效性來保證一致性,會試圖使用不一致狀態的對象。
  • Builder 的建造者模式:使用必須的參數調用構造器,得到一個 Builder 對象,再在 builder 對象上調用類似 setter 的方法設置各個可選參數,最後調用無參的 build 方法生成不可變對象,new Instance.Builder(必須參數).setter(可選參數).build()。
  • Builder 模式讓類的創建和表示分離,使得相同的創建過程可以創建不同的表示。

3、避免創建不必要的對象

  • 對於 String 類型,String s = new String(“”) 每次執行時都會創建一個新的實例,而使用 String s = “” 則不會,因爲對於虛擬機而言,包含相同的字符串字面常量會重用,而不是每次執行時都創建一個新的實例。
  • 優先使用基本類型而不是裝箱的基本類型,避免無意識的自動裝箱。

4、消除過期的對象引用

  • 緩存時優先使用 WeakHashMap,LinkedHashMap 這些數據結構,及時清掉沒用的項。
  • 顯示取消監聽器和回調,或進行弱引用。

對於所有對象都通用的方法

5、覆蓋 equals

  • 如果類具有自己特有的”邏輯相等”,但超類還沒有覆蓋 equals 以實現期望的行爲。
  • 高質量equals的方法
    • 使用 == 操作符檢查”參數是否爲這個對象的引用“。
    • 使用 instanceof 操作符檢查“參數是否爲正確的類型”。
    • 把參數轉換成正確的類型。
    • 對於該類中的每個關鍵域,檢查參數中的域是否與該對象中對應的域相匹配。
    • 不要將 equals 聲明的 object 對象替換爲其他的類型,因爲這樣是沒法覆蓋 Object.equals,只是提供了一個重載。

6、覆蓋 equals 時總是覆蓋 hashCode

  • 相等的對象必須具有相等的散列碼,如果沒有一起去覆蓋 hashcode,則會導致倆個相等的對象未必有相等的散列碼,造成該類無法結合所有基於散列的集合一起工作。

7、總是覆蓋 toString

  • Object 提供的 toString,實現是類名+@+散列碼的無符號十六進制。
  • 自己覆蓋的 toString,返回對象中包含的所有值得關注的信息。
  • 不足:當類被廣泛使用,一旦指定格式,那就會編寫出相應的代碼來解析這種字符串表示法,以及把字符串表示法嵌入持久化數據中,之後若改變這種表示法,則會遭到破壞。

8、考慮實現 Comparable 接口

  • 如果類實現了comparable 接口,便可以跟許多泛型算法以及依賴該接口的集合實現協作,比如可以使用 Array.sort 等集合的排序。

類和接口

9、使類和成員的可訪問性最小化

  • 隱藏內部實現細節,有效解耦各模塊的耦合關係
  • 訪問級別
    • private:類內部纔可訪問
    • package-private(缺省的):包內部的任何類可訪問
    • protected:聲明該成員的類的子類以及包內部的類可訪問
    • public:任何地方均可訪問

10、複合優於繼承

  • 繼承打破了封裝性,除非超類是專門爲了擴展而設計的。超類若在後續的發行版本中獲得新的方法,並且其子類覆蓋超類中與新方法有關的方法,則可能會發生錯誤。
  • 複合:在新的類中增加一個私有域,引用現有類。它不依賴現有類的實現細節,對現有類進行轉發。

11、接口優於抽象類

  • 抽象類允許包含某些方法的實現,但爲了實現由抽象類定義的類型,類必須成爲抽象類的一個子類,且是單繼承。
  • 接口允許我們構造非層次結構的類型框架,安全地增強類的功能。
  • 對每個重要的接口都提供一個抽象的骨架實現類,把接口和抽象類的優點結合(接口不能包含具體的方法,抽象類使用繼承來增加功能)。它們爲抽象類提供了實現上的幫助,但又不強加抽象類被用作類型定義時所特有的嚴格限制。
  • 抽象類的演變比接口的演變要容易得多,在後續版本中在抽象類中始終可以增加新的具體方法,其抽象類的所有子類都將提供這個新的方法,而接口不行。

12、接口只用於定義類型

  • 當類實現接口時,接口充當可以引用這個類的實例的類型,爲了任何其他目的而定義接口時不恰當的。
  • 常量接口時對接口的不良使用。實現常量接口,會導致把這樣的實現細節泄漏給該類的導出 API 中,當類不再需要這些常量時,還必須實現這個接口以確保兼容性。如果非final類實現了該常量接口,它的所有子類的命名空間都將被接口中的常量污染。

13、優先考慮靜態成員類

  • 靜態成員類是最簡單的嵌套類,可以當做普通的類,只是被聲明在另一個類的內部。
  • 非靜態成員類的每個實例都隱含着與外部類的一個外部實例相關聯。沒有外部實例的情況下,是無法創建非靜態成員類的實例。每個非靜態成員類的實例都包含一個額外的指向外部對象的引用,會導致外部實例在垃圾回收時仍然保留。
  • 匿名類沒有名字,在使用的同時被聲明和實例化。當匿名類出現在非靜態環境中時有外部實例,在靜態環境中也不能擁有任何靜態成員。匿名類必須保持簡短,保持可讀性。
  • 局部類,在任何可以聲明局部變量的地方聲明局部類,有名字,在非非靜態環境中定義纔有外部實例,不能包含靜態成員,同時必須保持簡短。

枚舉和註解

14、用 enum 代替 int 常量

  • 枚舉類型是指由一組固定的常量組成合法值的類型,通過公有的靜態 final 域爲每個枚舉常量導出實例的類,沒有構造器,是單例的泛型化。
  • int 枚舉模式在類型安全性和使用方便性沒有任何幫助,打印的 int 枚舉變量只是一個數字。
  • String 枚舉模式雖然提供了可打印的字符串,但會導致性能問題,還依賴於字符串的比較操作。
  • 枚舉類型可以通過 toString 將枚舉轉換成可打印的字符串,還允許添加任意的方法和域,並實現任意的接口。
  • 性能缺點:裝載和初始化枚舉時會有空間和時間的成本。

方法

15、檢查參數的有效性

  • 對於公有方法,用 Javadoc 的 @throw 標籤在文檔中說明違反參數限制時會拋出的異常。
  • 對於未被導出的方法(私有的),可以使用斷言來檢查參數。斷言如果失敗會拋出 AssertionException,如果沒起到作用也不會有成本開銷。
  • 每當編寫方法或構造器時,要考慮它的參數有哪些限制,應該把這些限制寫到文檔中,並且在方法體的開頭處進行顯示的檢查。

16、必要時進行保護性拷貝

  • 對方法的每個可變參數,或返回一個指向內部可變組件的引用時,需要進行保護性拷貝,避免在使用過程中可變對象進行了修改。
  • 保護性拷貝是在檢查參數的有效性之前進行的,並且有效性檢查是針對拷貝之後的對象。

17、 慎用重載

  • 重載方法的選擇是靜態的,選擇工作時在編譯時進行,完全基於參數的編譯時類型。
  • 覆蓋方法的選擇是動態的,選擇的依據是被調用方法所在對象的運行時類型。
  • 不要導出倆個具有相同參數數目的重載方法,如果參數數目相同,則至少有一個對應的參數在倆個重載方法中具有根本不同的類型,否則就應該保證,當傳遞同樣的參數時,所有的重載方法的行爲必須一致。

18、返回零長度的數組或集合,而不是 null

  • 對於返回 null 而不是零長度數組或集合的方法,幾乎每次用到該方法時都需要進行 null 值的判斷,這樣很曲折同時很容易出錯。

通用程序設計

19、基本類型優於裝箱基本類型

  • 基本類型只有值,而裝箱基本類型可以具有相同的值和不同的同一性。對裝箱基本類型運用 == 操作符幾乎總是錯誤的。
  • 基本類型只有功能完備的值,而每個裝箱基本類型除了它對應的基本類型的所有功能值外,還有個非功能值:null。當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型會自動拆箱,如果 null 對象引用被自動拆箱,會得到空指針異常。
  • 基本類型通常比裝箱基本類型更節省時間和空間,裝箱基本類型會導致高開銷和不必要的對象創建。

20、當心字符串連接的性能

  • 字符串是不可變的,當倆個字符串連接時需要對其內容進行拷貝,連接 n 個字符串需要 n 的平方級時間。因爲第 n 次拼接的字符串,需要 n-1 次的字符串和第 n 次的字符串拷貝,和他們拼接後的拷貝,這樣 an – an-1 = n-1+1+n = 2n;這樣可以得到 an = n*(n-1),及 O(N^2) 的拼接時間。

21、通過接口引用對象

  • 如果有合適的接口類型存在,那麼對於參數、返回值、變量和域來說,就都應該使用接口類型進行聲明。如,List<>vector = new Vector<>();List list = new ArrayList<>();,這樣程序會更加靈活,當更換實現時,所要做的只是改變構造器中的類。
  • 如果沒有合適的接口存在,完全可以用類而不是類接口來引用對象。如果含有基類,則優先使用基類來引用這個對象而不是它的實現類。

異常

22、只針對異常的情況才使用異常

  • 異常是爲了在異常情況下使用而設計的,不要將他們用於普通的控制流,而不要編寫破事他們這麼做的 API。
  • 基於異常的循環模式不僅模糊了代碼的意圖,降低了性能( JVM 不會對異常的代碼塊進行優化),而且它還不能保證正常工作。

23、對可恢復的情況使用受檢異常,對編程錯誤使用運行時異常

  • 受檢異常:如果期望調用者能適當地恢復,這時應該使用受檢的異常。通過拋出受檢的異常,強迫調用者在一個 catch 中處理該異常或傳播出去。
  • 未受檢異常:不需要也不應該被捕獲的可拋出結構。
    • 運行時異常:表明編程錯誤,是 RuntimeException 的子類,運行時檢查。
    • 錯誤:表示資源不足,約束失敗,或其他使程序無法繼續執行的條件。
  • 設計受檢異常拋出 API 的條件:正確地使用 API 不能阻止這種異常條件的產生 & 產生異常後可以立即採取有用的動作。

24、拋出與抽象相對應的異常

  • 當方法傳遞由低層抽象拋出的異常與所執行的任務沒有明顯聯繫時,會導致困擾且讓實現細節污染了更高層 API。
  • 更高層的實現應該捕獲低層的異常,同時拋出可以按照高層抽象進行解釋的異常(異常轉譯)。

25、努力使失敗保持原子性

  • 失敗原子性:失敗的方法調用應該使對象保持在被調用之前的狀態。
  • 設計不可變對象,永遠不會使已有的對象保持在不一致的狀態中。
  • 對於可變對象:
    • 執行操作之前檢查參數的有效性。
    • 調整計算處理過程的順序,使得任何可能失敗的計算部分都在對象狀態被修改之前發生。
    • 編寫一段恢復代碼,由它來攔截操作過程中發生的失敗,以及對象回滾到操作開始之前的狀態上,主要用於永久性的數據結構。
    • 在對象的一份臨時拷貝上執行操作,不破壞傳入對象的狀態。

併發

26、同步訪問

  • 同步可以阻止一個線程看到對象處於不一致的狀態之中,還能保證進入同步方法或者同步代碼塊的每個線程,都看到由同一個鎖保護的之前所有的修改效果。
  • 多個線程共享可變數據時,每個讀或者寫數據的線程都必須執行同步,否則可能導致活性失敗和安全性失敗。
    • 活性失敗:線程A對某變量值的修改,可能沒有立即在線程B體現出來。
    • 安全性失敗:併發訪問共享資源導致狀態不一致造成的安全問題。
  • 過度同步可能會導致性能降低、死鎖,甚至不確定的行爲。
    • 在同步區域內做盡可能少的工作,過度的同步會丟失並行的機會,限制 VM 優化代碼執行的能力
    • 不要從同步區域內部調用外來方法,避免死鎖和數據破壞。
    • CopyOnWriteArrayList 通過重新拷貝整個底層數組實現所有的寫操作,適用於讀操作遠大於寫操作的場景,當寫操作頻繁時性能損耗很大。

序列化

27、謹慎地實現 Serializable 接口

  • 一旦一個類被髮布,就大大降低了“改變這個類的實現” 的靈活性。若接受了默認的序列化形式,並且以後要改變類的內部結構,會導致序列化形式的不兼容。其次序列化對應流的唯一標識符 UID,在沒有顯示聲明序列版本 UID,那麼改變類的信息,將產生新的序列版本 UID,破壞它的兼容性。
  • 增加了出現 bug 和安全漏洞的可能性。反序列化機制中沒有顯示的構造器,很容易忘記要確保:反序列化過程必須要保證所有“由真正的構造器建立起來的約束關係”,並且不允許攻擊者訪問正在構造過程中的對象的內部信息。
  • 測試負擔增加。當一個可序列號的類被修訂時,需要檢查“在新版本中序列化一個實例,然後再舊版本中反序列號”,反之亦然,這種測試不可自動構建,測試工作量與“可序列化的類的數量和發行版本號”的乘積成正比。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章