Effective Java筆記一 建立和銷燬對象

Effective Java筆記一 建立和銷燬對象

  • 第1條 考慮用靜態工廠方法代替構造器
  • 第2條 遇到多個構造器參數時要考慮用構建器
  • 第3條 用私有構造器或者枚舉類型強化Singleton屬性
  • 第4條 經過私有構造器強化不可實例化的能力
  • 第5條 避免建立沒必要要的對象
  • 第6條 消除過時的對象引用
  • 第7條 避免使用終結方法

第1條 考慮用靜態工廠方法代替構造器

對於類而言, 最經常使用的獲取實例的方法就是提供一個公有的構造器, 還有一種方法, 就是提供一個公有的靜態工廠方法(static factory method), 返回類的實例.java

(注意此處的靜態工廠方法與設計模式中的工廠方法模式不一樣.)程序員

提供靜態工廠方法而不是公有構造, 這樣作有幾大優點:算法

  • 靜態工廠方法有名稱. 能夠更確切地描述正被返回的對象.
    當一個類須要多個帶有相同簽名的構造器時, 能夠用靜態工廠方法, 而且慎重地選擇名稱以便突出它們之間的區別.
  • 沒必要在每次調用它們的時候都建立一個新對象. 能夠重複利用實例. 若是程序常常請求建立相同的對象, 而且建立對象的代價很高, 這項改動能夠提高性能. (不可變類, 單例, 枚舉).
  • 能夠返回原類型的子類型對象. 適用於基於接口的框架, 能夠隱藏實現類API, 也能夠根據參數返回不一樣的子類型.
    因爲接口不能有靜態方法, 所以按照慣例, 接口Type的靜態工廠方法被放在一個名爲Types的不可實例化的類中.
    (Java的java.util.Collections). 服務提供者框架(Service Provider Framework, 如JDBC)的基礎, 從實現中解耦.
  • 在建立參數化類型實例的時候, 使代碼更簡潔.

靜態工廠方法的缺點:sql

  • 類若是不含public或者protected的構造器, 就不能被子類化. 對於公有的靜態工廠方法所返回的非公有類, 也一樣如此.
  • 靜態工廠方法與其餘的靜態方法沒有區別. 在API文檔中沒有明確標識出來. 可使用一些慣用的名稱來彌補這一劣勢:
    • valueOf(): 類型轉換方法, 返回的實例與參數具備相同的值.
    • of(): valueOf()的一種更簡潔的替代.
    • getInstance(): 返回的實例經過參數來描述, 對於單例來講, 該方法沒有參數, 返回惟一的實例.
    • newInstance(): 像getInstance()同樣, 但newInstance()能確保返回的每一個實例都與其餘實例不一樣.
    • getType(): 像getInstance()同樣, Type表示返回的對象類型, 在工廠方法處於不一樣的類中的時候使用.
    • newType(): 和newInstance()同樣, Type表示返回類型, 在工廠方法處於不一樣的類中的時候使用.

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

靜態工廠和構造器有一個共同的侷限性: 它們都不能很好地擴展到大量的可選參數.數據庫

重載多個構造器方法可行, 可是當有許多參數的時候, 代碼會很難寫難讀.設計模式

第二種替代方法是JavaBeans模式, 即一個無參數構造來建立對象, 而後調用setter方法來設置每一個參數. 這種模式也有嚴重的缺點, 由於構造過程被分到了幾個調用中, 在構造過程當中JavaBean可能處於不一致的狀態.
類沒法經過檢驗構造器參數的有效性來保證一致性. 另外一點是這種模式阻止了把類作成不可變的可能.數組

第三種方法就是Builder模式. 不直接生成想要的對象, 而是利用必要參數調用構造器(或者靜態工廠)獲得一個builder對象, 而後在builder對象上調用相似setter的方法, 來設置可選參數, 最後調用無參的build()方法來生成不可變的對象.緩存

這個Builder是它構建的類的靜態成員類.
Builder的setter方法返回Builder自己, 能夠鏈式操做.安全

Builder模式的優點: 可讀性加強; 能夠有多個可變參數; 易於作參數檢查和構造約束檢查; 比JavaBeans更加安全; 靈活性: 能夠利用單個builder構建多個對象, 能夠自動填充某些域, 好比自增序列號.框架

Builder模式的不足: 爲了建立對象必須先建立Builder, 在某些十分注重性能的狀況下, 可能就成了問題; Builder模式較冗長, 所以只有參數不少時才使用.

第3條 用私有構造器或者枚舉類型強化Singleton屬性

Singleton(單例)指僅僅被實例化一次的類. 一般用來表明那些本質上惟一的系統組件.

使類成爲Singleton會使得它的客戶端代碼測試變得困難, 由於沒法給它替換模擬實現, 除非它實現了一個充當其類型的接口.

單例的實現: 私有構造方法, 類中保留一個字段實例(static, final), 用public直接公開字段或者用一個public static的getInstance()方法返回該字段.

爲了使單例實現序列化(Serializable), 僅僅在聲明中加上implements Serializable是不夠的, 爲了維護並保證單例, 必須聲明全部實例域都是transient的, 並提供一個readResolve()方法, 返回單例的實例. 不然每次反序列化一個實例時, 都會建立一個新的實例.

從Java 1.5起, 可使用枚舉來實現單例: 只須要編寫一個包含單個元素的枚舉類型.
這種方法無償地提供了序列化機制, 絕對防止屢次實例化.

第4條 經過私有構造器強化不可實例化的能力

只包含靜態方法和靜態域的類名聲不太好, 由於有些人會濫用它們來編寫過程化的程序. 儘管如此, 它們確實也有特有的用處, 好比:
java.lang.Math, java.util.Arrays把基本類型的值或數組類型上的相關方法組織起來; java.util.Collections把實現特定接口的對象上的靜態方法組織起來; 還能夠利用這種類把final類上的方法組織起來, 以取代擴展該類的作法.

這種工具類(utility class)不但願被實例化, 然而在缺乏顯式構造器的狀況下, 系統會提供默認構造器, 可能會形成這些類被無心識地實例化.

經過作成抽象類來強制該類不可被實例化, 這是行不通的, 由於可能會形成"這個類是用來被繼承的"的誤解, 而繼承它的子類又能夠被實例化.

因此只要讓這個類包含一個私有的構造器, 它就不能被實例化了. 進一步地, 能夠在這個私有構造器中拋出異常.

這種作法還會致使這個類不能被子類化, 由於子類構造器必須顯式或隱式地調用super構造器. 在這種狀況下, 子類就沒有可訪問的超類構造器可調用了.

第5條 避免建立沒必要要的對象

通常來講, 最好能重用對象而不是每次須要的時候建立一個相同功能的新對象. 若是對象是不可變的(immutable), 它就始終能夠被重用.

好比應該用:

String s = "stringette";

而不是:

String s = new String("stringette"); // Don't do this

包含相同字符串的字面常量對象是會被重用的.

對於同時提供了靜態工廠方法和構造方法的不可變類, 一般可使用靜態工廠方法而不是構造器, 以免建立沒必要要的對象.
好比Boolean.valueOf().

除了重用不可變對象之外, 也能夠重用那些已知不會被修改的可變對象. 好比把一個方法中須要用到的不變的數據保存成常量對象(static final), 只在初始化的時候建立一次(用static塊), 這樣就不用每次調用方法都重複建立.

若是該方法永遠不會調用, 那也不須要初始化相關的字段, 能夠經過延遲初始化(lazily initializing)把這些對象的初始化放到方法第一次被調用的時候. (可是不建議這樣作, 沒有性能的顯著提升, 而且會使方法看起來複雜.)

前面的例子中, 所討論的對象顯然是可以被重用的, 由於它們被初始化以後不會再改變. 其餘有些情形則並不老是這麼明顯了. (適配器(adapter)模式, Map的接口keySet()方法返回一樣的Set實例).

Java 1.5中加入了自動裝箱(autoboxing), 會建立對象. 因此程序中優先使用基本類型而不是裝箱基本類型, 要小心無心識的自動裝箱.

小對象的構造器只作不多量的顯式工做, 建立和回收都是很廉價的, 因此經過建立附加的對象提高程序的清晰簡潔性也是好事.

經過維護本身的對象池(object pool)來避免建立對象並非一種好的作法(代碼, 內存), 除非池中的對象是很是重量級的. 正確使用的典型: 數據庫鏈接池.

第6條 消除過時的對象引用

一個內存泄露的例子: 一個用數組實現的Stack, 依靠size標記來管理棧的深度, 可是這樣從棧中彈出來的過時對象並無被釋放.

稱內存泄露爲"無心識的對象保持(unintentional object retention)"更爲恰當.

修復方法: 一旦對象引用已通過期, 只需清空這些引用便可.

清空對象引用應該是一種例外, 而不是一種規範行爲. 消除過時引用最好的方法是讓包含該引用的變量結束其生命週期. 若是你是在最緊湊的做用域範圍內定義變量, 這種情形就會天然發生.

通常而言, 只要類是本身管理內存, 程序員就應該警戒內存泄露問題. 一旦元素被釋放掉, 則該元素中包含的任何對象引用都應該被清空.

內存泄露的另外一個常見來源是緩存. 這個問題有這幾種可能的解決方案:

  • 1.緩存項的生命週期由該鍵的外部引用決定 -> WeakHashMap;
  • 2.緩存項的生命週期是否有意義並非很容易肯定 -> 隨着時間的推移或者新增項的時候刪除沒用的項.

內存泄露的第三個常見來源是監聽器和其餘回調.
若是你實現了一個API, 客戶端註冊了回調卻沒有註銷, 就會積聚對象.
API端能夠只保存對象的弱引用來確保回調對象生命週期結束後會被垃圾回收.

第7條 避免使用終結方法

終結方法(finalizer)一般是不可預測的, 也是很危險的, 通常狀況下是沒必要要的.
使用終結方法會致使行爲不穩定, 下降性能, 以及可移植性問題.

不要把finalizer當成是C++中的析構器(destructors)的對應物.
在Java中, 當一個對象變得不可到達的時候, 垃圾回收器會回收與該對象相關聯的存儲空間.

C++的析構器也能夠用來回收其餘的非內存資源, 而在Java中, 通常用try-finally塊來完成相似的工做.

終結方法的缺點在於不能保證會被及時地執行. 從一個對象變得不可到達開始, 到它的終結方法被執行, 所花費的時間是任意長的. JVM會延遲執行終結方法.

及時地執行終結方法正是垃圾回收算法的一個主要功能. 這種算法在不一樣的JVM上不一樣.

Java語言規範不只不保證終結方法會被及時地執行, 並且根本就不保證它們會被執行. 因此不該該依賴於終結方法來更新重要的持久狀態.

不要被System.gc()System.runFinalization()這兩個方法所迷惑, 它們確實增長了終結方法被執行的機會, 可是它們並不保證終結方法必定會被執行.

若是未捕獲的異常在終結過程當中被拋出來, 那麼這種異常能夠被忽略, 並且該對象的終結過程也會終止.

使用終結方法有一個嚴重的性能損失.

若是類的對象中封裝的資源(例如文件或線程)確實須要終止, 應該怎麼作才能不用編寫終結方法呢? 只需提供一個顯式的終止方法. 並要求該類的客戶端在每一個實例再也不有用的時候調用這個方法. 注意, 該實例必須記錄下本身是否已經被終止了, 若是被終止以後再被調用, 要拋出異常.
例子: InputStream, OutputStreamjava.sql.Connection上的close()方法; java.util.Timercancel()方法.
Image.flush()會釋放實例相關資源, 但該實例仍處於可用的狀態, 若是有必要會從新分配資源.

顯式的終止方法一般與try-finally塊結合使用, 以確保及時終止.

終結方法的好處, 它有兩種合法用途:

  • 當顯式終止方法被忘記調用時, 終結方法能夠充當安全網(safety net). 可是若是終結方法發現資源還未被終止, 應該記錄日誌警告, 這表示客戶端代碼中的bug.
  • 對象的本地對等體(native peer), 垃圾回收器不會知道它, 當它的Java對等體被回收的時候, 它不會被回收. 若是本地對等體擁有必須被及時終止的資源, 那麼該類就應該有一個顯式的終止方法, 如前, 能夠是本地方法或者它也能夠調用本地方法; 若是本地對等體並不擁有關鍵資源, 終結方法是執行這項任務最合適的工具.

注意, 終結方法鏈(finalizer chaining)並不會自動執行. 子類覆蓋終結方法時, 必須手動調用超類的終結方法. try中終結子類, finally中終結超類.

爲了不忘記調用超類的終結方法, 還有一種寫法, 是在子類中寫一個匿名的類, 該匿名類的單個實例被稱爲終結方法守衛者(finalizer guardian), 當守衛者被終結的時候, 它執行外圍實例的終結行爲. 這樣外圍類並無覆蓋超類的終結方法, 保證了超類的終結方法必定會被執行.

相關文章
相關標籤/搜索