Effective Java 之我的總結

建立和銷燬對象

一、靜態工廠方法代替構造器
  • 靜態工廠方法有名稱,能確切地描述正被返回的對象。
  • 沒必要每次調用都建立一個新的對象。
  • 能夠返回原返回類型的任何子類對象。
  • 建立參數化類型實例時更加簡潔,好比調用構造 HashMap 時,使用 Map<String,List<String> m = HashMap.newInstance() ,與 Map<String,List<String>m> = new HashMap<String,List<String>>();
二、遇到多個構造器參數時要考慮用構建器
  • 靜態工廠和構造器不能很好地擴展到大量的可選參數。
  • JavaBean 模式下使用 setter 來設置各個參數,沒法僅經過檢驗構造器參數的有效性來保證一致性,會試圖使用不一致狀態的對象。
  • Builder 的建造者模式:使用必須的參數調用構造器,獲得一個 Builder 對象,再在 builder 對象上調用相似 setter 的方法設置各個可選參數,最後調用無參的 build 方法生成不可變對象,new Instance.Builder(必須參數).setter(可選參數).build()。
  • Builder 模式讓類的建立和表示分離,使得相同的建立過程能夠建立不一樣的表示。
三、避免建立沒必要要的對象
  • 對於 String 類型,String s = new String("") 每次執行時都會建立一個新的實例,而使用 String s = "" 則不會,由於對於虛擬機而言,包含相同的字符串字面常量會重用,而不是每次執行時都建立一個新的實例。
  • 優先使用基本類型而不是裝箱的基本類型,避免無心識的自動裝箱。
四、消除過時的對象引用
  • 緩存時優先使用 WeakHashMap,LinkedHashMap 這些數據結構,及時清掉沒用的項。
  • 顯示取消監聽器和回調,或進行弱引用。

對於全部對象都通用的方法

五、覆蓋 equals
  • 若是類具備本身特有的"邏輯相等",但超類尚未覆蓋 equals 以實現指望的行爲。
  • 高質量equals的方法git

    • 使用 == 操做符檢查」參數是否爲這個對象的引用「。
    • 使用 instanceof 操做符檢查「參數是否爲正確的類型」。
    • 把參數轉換成正確的類型。
    • 對於該類中的每一個關鍵域,檢查參數中的域是否與該對象中對應的域相匹配。
    • 不要將 equals 聲明的 object 對象替換爲其餘的類型,由於這樣是無法覆蓋 Object.equals,只是提供了一個重載。
六、覆蓋 equals 時老是覆蓋 hashCode
  • 相等的對象必須具備相等的散列碼,若是沒有一塊兒去覆蓋 hashcode,則會致使倆個相等的對象未必有相等的散列碼,形成該類沒法結合全部基於散列的集合一塊兒工做。
七、老是覆蓋 toString
  • Object 提供的 toString,實現是類名+@+散列碼的無符號十六進制。
  • 本身覆蓋的 toString,返回對象中包含的全部值得關注的信息。
  • 不足:當類被普遍使用,一旦指定格式,那就會編寫出相應的代碼來解析這種字符串表示法,以及把字符串表示法嵌入持久化數據中,以後若改變這種表示法,則會遭到破壞。
八、考慮實現 Comparable 接口
  • 若是類實現了comparable 接口,即可以跟許多泛型算法以及依賴該接口的集合實現協做,好比可使用 Array.sort 等集合的排序。

類和接口

九、使類和成員的可訪問性最小化
  • 隱藏內部實現細節,有效解耦各模塊的耦合關係
  • 訪問級別github

    • private:類內部纔可訪問
    • package-private(缺省的):包內部的任何類可訪問
    • protected:聲明該成員的類的子類以及包內部的類可訪問
    • public:任何地方都可訪問
十、複合優於繼承
  • 繼承打破了封裝性,除非超類是專門爲了擴展而設計的。超類若在後續的發行版本中得到新的方法,而且其子類覆蓋超類中與新方法有關的方法,則可能會發生錯誤。
  • 複合:在新的類中增長一個私有域,引用現有類。它不依賴現有類的實現細節,對現有類進行轉發。
十一、接口優於抽象類
  • 抽象類容許包含某些方法的實現,但爲了實現由抽象類定義的類型,類必須成爲抽象類的一個子類,且是單繼承。
  • 接口容許咱們構造非層次結構的類型框架,安全地加強類的功能。
  • 對每一個重要的接口都提供一個抽象的骨架實現類,把接口和抽象類的優勢結合(接口不能包含具體的方法,抽象類使用繼承來增長功能)。它們爲抽象類提供了實現上的幫助,但又不強加抽象類被用做類型定義時所特有的嚴格限制。
  • 抽象類的演變比接口的演變要容易得多,在後續版本中在抽象類中始終能夠增長新的具體方法,其抽象類的全部子類都將提供這個新的方法,而接口不行。
十二、接口只用於定義類型
  • 當類實現接口時,接口充當能夠引用這個類的實例的類型,爲了任何其餘目的而定義接口時不恰當的。
  • 常量接口時對接口的不良使用。實現常量接口,會致使把這樣的實現細節泄漏給該類的導出 API 中,當類再也不須要這些常量時,還必須實現這個接口以確保兼容性。若是非final類實現了該常量接口,它的全部子類的命名空間都將被接口中的常量污染。
1三、優先考慮靜態成員類
  • 靜態成員類是最簡單的嵌套類,能夠當作普通的類,只是被聲明在另外一個類的內部。
  • 非靜態成員類的每一個實例都隱含着與外部類的一個外部實例相關聯。沒有外部實例的狀況下,是沒法建立非靜態成員類的實例。每一個非靜態成員類的實例都包含一個額外的指向外部對象的引用,會致使外部實例在垃圾回收時仍然保留。
  • 匿名類沒有名字,在使用的同時被聲明和實例化。當匿名類出如今非靜態環境中時有外部實例,在靜態環境中也不能擁有任何靜態成員。匿名類必須保持簡短,保持可讀性。
  • 局部類,在任何能夠聲明局部變量的地方聲明局部類,有名字,在非非靜態環境中定義纔有外部實例,不能包含靜態成員,同時必須保持簡短。

枚舉和註解

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

方法

1五、檢查參數的有效性
  • 對於公有方法,用 Javadoc 的 @throw 標籤在文檔中說明違反參數限制時會拋出的異常。
  • 對於未被導出的方法(私有的),可使用斷言來檢查參數。斷言若是失敗會拋出 AssertionException,若是沒起到做用也不會有成本開銷。
  • 每當編寫方法或構造器時,要考慮它的參數有哪些限制,應該把這些限制寫到文檔中,而且在方法體的開頭處進行顯示的檢查。
1六、必要時進行保護性拷貝
  • 對方法的每一個可變參數,或返回一個指向內部可變組件的引用時,須要進行保護性拷貝,避免在使用過程當中可變對象進行了修改。
  • 保護性拷貝是在檢查參數的有效性以前進行的,而且有效性檢查是針對拷貝以後的對象。
1七、 慎用重載
  • 重載方法的選擇是靜態的,選擇工做時在編譯時進行,徹底基於參數的編譯時類型。
  • 覆蓋方法的選擇是動態的,選擇的依據是被調用方法所在對象的運行時類型。
  • 不要導出倆個具備相同參數數目的重載方法,若是參數數目相同,則至少有一個對應的參數在倆個重載方法中具備根本不一樣的類型,不然就應該保證,當傳遞一樣的參數時,全部的重載方法的行爲必須一致。
1八、返回零長度的數組或集合,而不是 null
  • 對於返回 null 而不是零長度數組或集合的方法,幾乎每次用到該方法時都須要進行 null 值的判斷,這樣很曲折同時很容易出錯。

通用程序設計

1九、基本類型優於裝箱基本類型
  • 基本類型只有值,而裝箱基本類型能夠具備相同的值和不一樣的同一性。對裝箱基本類型運用 == 操做符幾乎老是錯誤的。
  • 基本類型只有功能完備的值,而每一個裝箱基本類型除了它對應的基本類型的全部功能值外,還有個非功能值:null。當在一項操做中混合使用基本類型和裝箱基本類型時,裝箱基本類型會自動拆箱,若是 null 對象引用被自動拆箱,會獲得空指針異常。
  • 基本類型一般比裝箱基本類型更節省時間和空間,裝箱基本類型會致使高開銷和沒必要要的對象建立。
20、小心字符串鏈接的性能
  • 字符串是不可變的,當倆個字符串鏈接時須要對其內容進行拷貝,鏈接 n 個字符串須要 n 的平方級時間。由於第 n 次拼接的字符串,須要 n-1 次的字符串和第 n 次的字符串拷貝,和他們拼接後的拷貝,這樣 an - an-1 = n-1+1+n = 2n;這樣能夠獲得 an = n*(n-1),及 O(N^2) 的拼接時間。
2一、經過接口引用對象
  • 若是有合適的接口類型存在,那麼對於參數、返回值、變量和域來講,就都應該使用接口類型進行聲明。如,List<>vector = new Vector<>();List list = new ArrayList<>(); ,這樣程序會更加靈活,當更換實現時,所要作的只是改變構造器中的類。
  • 若是沒有合適的接口存在,徹底能夠用類而不是類接口來引用對象。若是含有基類,則優先使用基類來引用這個對象而不是它的實現類。

異常

2二、只針對異常的狀況才使用異常
  • 異常是爲了在異常狀況下使用而設計的,不要將他們用於普通的控制流,而不要編寫破事他們這麼作的 API。
  • 基於異常的循環模式不只模糊了代碼的意圖,下降了性能( JVM 不會對異常的代碼塊進行優化),並且它還不能保證正常工做。
2三、對可恢復的狀況使用受檢異常,對編程錯誤使用運行時異常
  • 受檢異常:若是指望調用者能適當地恢復,這時應該使用受檢的異常。經過拋出受檢的異常,強迫調用者在一個 catch 中處理該異常或傳播出去。
  • 未受檢異常:不須要也不該該被捕獲的可拋出結構。算法

    • 運行時異常:代表編程錯誤,是 RuntimeException 的子類,運行時檢查。
    • 錯誤:表示資源不足,約束失敗,或其餘使程序沒法繼續執行的條件。
  • 設計受檢異常拋出 API 的條件:正確地使用 API 不能阻止這種異常條件的產生 & 產生異常後能夠當即採起有用的動做。
2四、拋出與抽象相對應的異常
  • 當方法傳遞由低層抽象拋出的異常與所執行的任務沒有明顯聯繫時,會致使困擾且讓實現細節污染了更高層 API。
  • 更高層的實現應該捕獲低層的異常,同時拋出能夠按照高層抽象進行解釋的異常(異常轉譯)。
2五、努力使失敗保持原子性
  • 失敗原子性:失敗的方法調用應該使對象保持在被調用以前的狀態。
  • 設計不可變對象,永遠不會使已有的對象保持在不一致的狀態中。
  • 對於可變對象:編程

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

併發

2六、同步訪問
  • 同步能夠阻止一個線程看到對象處於不一致的狀態之中,還能保證進入同步方法或者同步代碼塊的每一個線程,都看到由同一個鎖保護的以前全部的修改效果。
  • 多個線程共享可變數據時,每一個讀或者寫數據的線程都必須執行同步,不然可能致使活性失敗和安全性失敗。數組

    • 活性失敗:線程A對某變量值的修改,可能沒有當即在線程B體現出來。
    • 安全性失敗:併發訪問共享資源致使狀態不一致形成的安全問題。
  • 過分同步可能會致使性能下降、死鎖,甚至不肯定的行爲。緩存

    • 在同步區域內作儘量少的工做,過分的同步會丟失並行的機會,限制 VM 優化代碼執行的能力
    • 不要從同步區域內部調用外來方法,避免死鎖和數據破壞。
    • CopyOnWriteArrayList 經過從新拷貝整個底層數組實現全部的寫操做,適用於讀操做遠大於寫操做的場景,當寫操做頻繁時性能損耗很大。

序列化

2七、謹慎地實現 Serializable 接口
  • 一旦一個類被髮布,就大大下降了「改變這個類的實現」 的靈活性。若接受了默認的序列化形式,而且之後要改變類的內部結構,會致使序列化形式的不兼容。其次序列化對應流的惟一標識符 UID,在沒有顯示聲明序列版本 UID,那麼改變類的信息,將產生新的序列版本 UID,破壞它的兼容性。
  • 增長了出現 bug 和安全漏洞的可能性。反序列化機制中沒有顯示的構造器,很容易忘記要確保:反序列化過程必需要保證全部「由真正的構造器創建起來的約束關係」,而且不容許攻擊者訪問正在構造過程當中的對象的內部信息。
  • 測試負擔增長。當一個可序列號的類被修訂時,須要檢查「在新版本中序列化一個實例,而後再舊版本中反序列號」,反之亦然,這種測試不可自動構建,測試工做量與「可序列化的類的數量和發行版本號」的乘積成正比。

本文發表於我的博客:http://lavnfan.github.io/,歡迎指教。安全

相關文章
相關標籤/搜索