Effective Java筆記一 創建和銷毀對象
- 第1條 考慮用靜態工廠方法代替構造器
- 第2條 遇到多個構造器參數時要考慮用構建器
- 第3條 用私有構造器或者枚舉類型強化Singleton屬性
- 第4條 通過私有構造器強化不可實例化的能力
- 第5條 避免創建不必要的對象
- 第6條 消除過期的對象引用
- 第7條 避免使用終結方法
第1條 考慮用靜態工廠方法代替構造器
對於類而言, 最常用的獲取實例的方法就是提供一個公有的構造器, 還有一種方法, 就是提供一個公有的靜態工廠方法(static factory method), 返回類的實例.
(注意此處的靜態工廠方法與設計模式中的工廠方法模式不同.)
提供靜態工廠方法而不是公有構造, 這樣做有幾大優勢:
- 靜態工廠方法有名稱. 可以更確切地描述正被返回的對象.
當一個類需要多個帶有相同簽名的構造器時, 可以用靜態工廠方法, 並且慎重地選擇名稱以便突出它們之間的區別. - 不必在每次調用它們的時候都創建一個新對象. 可以重復利用實例. 如果程序經常請求創建相同的對象, 並且創建對象的代價很高, 這項改動可以提升性能. (不可變類, 單例, 枚舉).
- 可以返回原類型的子類型對象. 適用於基於接口的框架, 可以隱藏實現類API, 也可以根據參數返回不同的子類型.
由於接口不能有靜態方法, 因此按照慣例, 接口Type的靜態工廠方法被放在一個名為Types的不可實例化的類中.
(Java的java.util.Collections). 服務提供者框架(Service Provider Framework, 如JDBC)的基礎, 從實現中解耦. - 在創建參數化類型實例的時候, 使代碼更簡潔.
靜態工廠方法的缺點:
- 類如果不含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
, OutputStream
和java.sql.Connection
上的close()
方法; java.util.Timer
的cancel()
方法.
Image.flush()
會釋放實例相關資源, 但該實例仍處於可用的狀態, 如果有必要會重新分配資源.
顯式的終止方法通常與try-finally塊結合使用, 以確保及時終止.
終結方法的好處, 它有兩種合法用途:
- 當顯式終止方法被忘記調用時, 終結方法可以充當安全網(safety net). 但是如果終結方法發現資源還未被終止, 應該記錄日志警告, 這表示客戶端代碼中的bug.
- 對象的本地對等體(native peer), 垃圾回收器不會知道它, 當它的Java對等體被回收的時候, 它不會被回收. 如果本地對等體擁有必須被及時終止的資源, 那么該類就應該有一個顯式的終止方法, 如前, 可以是本地方法或者它也可以調用本地方法; 如果本地對等體並不擁有關鍵資源, 終結方法是執行這項任務最合適的工具.
注意, 終結方法鏈(finalizer chaining)並不會自動執行. 子類覆蓋終結方法時, 必須手動調用超類的終結方法. try中終結子類, finally中終結超類.
為了避免忘記調用超類的終結方法, 還有一種寫法, 是在子類中寫一個匿名的類, 該匿名類的單個實例被稱為終結方法守衛者(finalizer guardian), 當守衛者被終結的時候, 它執行外圍實例的終結行為. 這樣外圍類並沒有覆蓋超類的終結方法, 保證了超類的終結方法一定會被執行.