Effective Java
創建和銷毀對象---考慮用靜態工廠方法代替構造器
構造器是創建一個對象實例最基本也最通用的方法,大部分開發者在使用某個class的時候,首先需要考慮的就是如何構造和初始化一個對象示例,而構造的方式首先考慮到的就是通過構造函數來完成,因此在看javadoc中的文檔時首先關注的函數也是構造器。所以對於類而言,我們為了獲得一個類的實例對象,通常情況下會提供一個公有的(public) 的構造器。當然除了這種方法以外,我們還可以通過給類提供一個public的靜態工廠方法(static factory method)的方式來完成,讓它返回一個類的實例。如:
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
這個示例將boolean基本類型值轉換成一個Boolean對象的引用。(需要注意的是這里的靜態工廠方法與設計模式中所說的工廠方法模式不同)那么,在Effective Java的首篇中,我們來看看靜態工廠方法相比於構造器帶來了哪些優勢:1. 靜態工廠方法可以有不同的名稱,代碼更清楚,更易讀。在框架設計中,針對某些工具類通常會考慮空對象以辨別該對象是否已經被初始化。構造器的命名都一致,一個類只能有一個指定簽名的構造器。當一個類需要提供多個構造器時,通常只是通過不同的形參類型的順序加以區分,但其函數名還是相同的,無法提供較高的區分度。那么,當一個類需要多個帶有相同簽名的構造器時,不妨考慮用靜態工廠方法代替構造器,並且慎重地選擇名稱以便突出它們之間的區別。這樣該靜態工廠方法有名稱,通過賦予有意義的名稱,使用該方法的程序員可以清晰的知道該方法的含義。(方法的簽名(signature)由它的名稱和所有參數的返回類型組成,而不包括它的返回類型)2. 靜態工廠方法不必在每次調用它們的時候都創建一個新對象。比如你可以在靜態工廠方法里限定創建該對象的個數,當超出規定的個數時,返回緩存里的對象。場景:調用構造器時每次都創建新對象。這一點很顯然,我們知道Singleton其實也提供一個靜態工廠方法獲取實例。除此之外,可以用==代替equals()方法,達到性能的提升。3. 靜態工廠方法可以返回原返回類型的任何子類型的對象。場景:構造器只能返回當前類的實例,無法返回子類的實例。這個優點的確很有用,能夠充分利用多態,使代碼更具有可擴展性。設計模式中的工廠方法也有體現,可根據入參type返回類型為Types的不同子類實例。
public Types getType(String type) {...}
這樣我們在選擇返回對象的類時就有更大的靈活性,這種靈活性的應用就是API可以返回對象,同時又不會使對象的類變成公有的。以這種方式隱藏實現類會使API變得簡潔。這項技術適合於基於接口的框架(interface-based framework)。這種面向接口的編程也提高了軟件的可維護性和性能。4. 靜態工廠方法在創建參數化類型實例的時候使代碼變得更加簡潔。場景: Java的泛型類在實例化時,還是需要寫兩次類型參數,非常冗長。靜態工廠方法可以幫你改善這種情況:舉個例子,假設在HashMap類中提供如下靜態工廠方法:
public static <k, v=""> HashMap<k, v=""> newInstance(){ return new HashMap<k, v="">(); }
那么,調用時應該是這樣的:
HashMap<stirng, list<string="">> m = HashMap.newInstance();
而在調用參數化的構造器時,則通常需要這樣做,顯得比較繁瑣。
HashMap<string, list<string="">> m = new HashMap<string, list<string="">>();
另外值得一提的是,靜態工廠方法返回的對象所屬的類,在編寫包含該靜態工廠方法的類時不必存在。這種靈活的靜態工廠方法構成了服務提供者框架(Service Provider Framework)的基礎,例如JDBC的API。所謂服務提供者框架是指這樣的一個系統:多個服務提供者實現同一個服務,由框架(即系統)來負責為客戶端提供這種服務的多個實現並將這些實現解耦。服務提供者框架有三個主要的組件:1.服務接口(Service interface),這是提供者實現的。2.提供者注冊API(Provider Registration API),這是系統用來注冊實現,讓客戶端訪問它們的。3.服務訪問API(Service Access API),這是客戶端獲取服務實例用的。另外還有第四個可選接口,服務提供者接口(Service Provider Interface),負責創建其服務實現的實例。若沒有服務提供者接口,實現就按照類名稱進行注冊,並通過反射方式進行實例化。對JDBC而言,Connection就是它的服務接口,DriverManager.registerDriver就是它的提供者注冊API,DriverManager.getConnection是服務訪問API,Driver就是服務提供者接口。
如提供了兩個靜態工廠方法empty和preallocate(含義,重新分配;再指派)用於分別創建一個空對象和一個帶有指定分配空間的String對象。從使用方式來看,這些靜態方法確實提供了有使用者很容易就可以判斷出它們的作用和應用場景,而不必在一組重載的構造器中去搜尋每一個構造函數及其參數列表,以找出適合當前場景的構造函數。
從效率方面來講,由於提供了唯一的靜態空對象,當判讀對象實例是否為空時(isEmpty),直接使用預制靜態空對象(emptyString)的地址與當前對象進行比較,如果是同一地址,即可確認當前實例為空對象了。對於preallocate函數,顧名思義,該函數預分配了指定大小的內存空間,后面在使用該String實例時,不必擔心賦值或追加的字符過多而導致頻繁的realloc(重新分配內存)等操作。2.不必在每次調用它們的時候創建一個新的對象。還是基於上面的代碼實例,由於所有的空對象都共享同一個靜態空對象,這樣也節省了更多的內存開銷,如果是strEmpty2方式構造出的空對象,在執行比較等操作時會帶來更多的效率開銷。事實上,Java在String對象的實現中,使用了常量資源池也是基於了同樣的優化策略。該優勢同樣適用於單實例模式。3.可以返回原返回類型的任何子類型。在Java Collections Framework的集合接口中,提供了大量的靜態方法返回集合接口類型的實現類型,如Collections.subList()、Collections.unmodifiableList()等。返回的接口是明確的,然而針對具體的實現類,函數的使用者並不也無需知曉。這樣不僅極大的減少了導出類的數量,而且在今后如果發現某個子類的實現效率較低或者發現更好的數據結構和算法來替換當前實現子類時,對於集合接口的使用者來說,不會帶來任何的影響。本書在例子中提到EnumSet是通過靜態工廠方法返回對象實例的,沒有提供任何構造函數,其內部在返回實現類時做了一個優化,即如果枚舉的數量小於64,該工廠方法將返回一個經過特殊優化的實現類實例(RegularEnumSet),其內部使用long(64bits在Java中)中的不同位來表示不同的枚舉值。如果枚舉的數量大於64,將使用long的數組作為底層支撐。然而這些內部實現類的優化對於使用者來說是透明的。4.在創建參數化類型實例的時候,它們使代碼變得更加簡潔。Map<String,String> m = new HashMap<String,String>(); 由於Java在構造函數的調用中無法進行類型的推演,因此也就無法通過構造器的參數類型來實例化指定類型參數的實例化對象。然而通過靜態工廠方法則可以利用參數類型推演的優勢,避免了類型參數在一次聲明中被多次重寫所帶來的煩憂,見如下代碼: