文章作者:Tyan
博客:noahsnail.com
第二章
這章是關于創建和銷毀對象的:什么時候怎樣創建它們,什么時候怎樣避免創建它們,怎樣確保它們被及時的銷毀,怎么管理任何清理操作,清理操作必須在對象銷毀之前。
Item 1: 考慮用靜態工廠方法代替構造函數
一個類允許客戶獲得它本身的一個實例通常的方式是提供一個公有的構造函數。還有另一種技術應該成為每個程序員工具箱中的一部分。一個類可以提供一種公有的static factory method
,static factory method
是一種簡單的靜態方法,它會返回一個類的實例。這有一個來自Boolean(基本類型boolean的封裝類)的簡單例子。這個方法將一個布爾值轉成Boolean對象的引用:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
注意靜態工廠方法與Design Patterns
中的Factory Method
是不同的。這個條目中描述的靜態工廠方法與設計模式中的工廠方法是不等價的。
一個類可以為它的客戶提供靜態工廠方法來代替構造函數,或者除了構造函數之外再提供一個靜態工廠方法。提供靜態工廠方法代替公有構造函數既有優點也有缺點。
與構造函數相比,靜態工廠方法的第一個優勢是它們有名字。如果構造函數的參數本身不能描述返回的對象,具有合適名字的靜態工廠是更容易使用的,并且產生的客戶端代碼更易讀。例如,構造函數BigInteger(int, int, Random)
返回一個BigInteger
,這個BigInteger
可能是一個素數,使用名字為BigInteger.probablePrime
的靜態工廠方法來表示會更好。(這個方法最終在1.4版本被引入。)
一個類只能有一個具有指定簽名的構造函數。程序員知道怎樣規避這個限制:通過提供兩個構造函數,它們僅在參數列表類型的順序上有所不同。這真的是一個壞主意。使用這種API的用戶永遠不能記住哪一個構造函數是哪一個,最后會無意中調用錯誤的構造函數。使用這些構造函數的人在讀代碼時如果沒有類的參考文檔將不知道代碼要做什么。
因為靜態工廠方法有名字,因此它們不會有上一段討論的那種限制。當一個類似乎需要多個具有相同簽名的構造函數時,用靜態工廠方法代替構造函數,通過仔細選擇工廠方法的名字來突出它們的不同。
與構造函數相比,靜態工廠方法的第二個優勢是當調用靜態工廠方法時不要求每次都創建一個新的對象。這允許不可變類(Item 15)使用預創建的實例,或緩存構建好的實例,通過重復分發它們避免創建不必要的重復對象。Boolean.valueOf(boolean)
方法闡明了這個技術:它從未創建對象。這項技術與Flyweight模式類似[Gamma95, p. 195]。如果經常請求相同的對象,它能極大的提升性能,尤其是在創建對象的代價較昂貴時。
靜態工廠方法能從重復的調用中返回相同的對象,在任何時候都能使類嚴格控制存在的實例。這些類被稱為控制實例。編寫控制實例類是有一些原因的。實例控制允許一個類保證它是一個單例(Item 3)或不可實例化的(Item 4)。它也允許一個不變的類(Item 15)保證不存在兩個相等的實例:a.equals(b)
當且僅當a==b
。如果一個類保證了這一點,它的客戶端可以使用==
操作符代替equals(Object)
方法,這可能會導致性能的提升。Enum類型(Item 30)保證了這一點。
與構造函數相比,靜態工廠方法的第三個優勢是它們能返回它們的返回類型的任意子類型的對象。這樣在選擇返回對象的類時有了更大的靈活性。
靈活性的一個應用是API能返回對象而不必使它們的類變成公有的。通過這種方式中隱藏實現類會有一個更簡潔的API。這項技術適用于基于接口的框架(Item 18),接口為靜態工廠方法提供了自然的返回類型。接口不能有靜態方法,因此按慣例,命名為Type
的接口的靜態工廠方法被放在一個命名為Types
的不可實例化的類中(Item 4)。
例如,Java集合框架有三十二個集合接口的便利實現,提供了不可修改的集合,同步集合等等。幾乎所有的這些實現都是通過靜態工廠方法導出在一個不可實例化的類中(java.util.Collections
)。返回對象的類都是非公有的。
集合框架API比它導出的三十二個分開的公有類更小,每一個便利實現對應一個類。它不僅僅是API的數量在減少,還是概念上意義上的減少。用戶知道返回的對象含有接口指定的精確API,因此不需要閱讀額外的實現類的文檔。此外,使用這樣的靜態工廠方法需要客戶端使用接口引用返回的對象而不是使用它的實現類,這通常是最佳的實踐(Item 52)。
不僅公有靜態工廠方法返回對象的類可以是非公有的,而且這個類還可以隨著調用靜態工廠時輸入的參數值的變化而變化。聲明的返回值類型的任何子類都是可以的。為了增強軟件的可維護性及性能,返回值對象的類也可以隨著發布版本的變化而變化。
在1.5版本中引入類java.util.EnumSet
(Item 32),它沒有公有的構造函數,只有靜態工廠方法。根據枚舉類型的大小,靜態工廠方法返回兩個實現中的一個,枚舉類型的分類:如果枚舉類型中有六十四個元素或更少,與大多數枚舉類型一樣,靜態工廠返回一個RegularEnumSet
實例,由單個的long
支持;如果枚舉類型中有六十五個元素或更多,靜態工廠方法返回一個JumboEnumSet
實例,由long[]
支持。
現有的兩個實現類對于客戶端是不可見的。如果RegularEnumSet
對于較少數量的枚舉類型沒有提供性能優勢,那么在將來的版本中將其移除不會任何影響。同樣地,如果新的EnumSet
實現在性能上更有優勢,在將來的版本中添加EnumSet
的第三或第四個實現也不會有任何影響。客戶端不知道也不關心它們從工廠方法中得到的對象所屬的類;它們只關心它是EnumSet
的某個子類。
在編寫靜態工廠方法所屬的類時,靜態工廠方法返回的對象所屬的類可以不必存在。這種靈活的靜態工廠方法形成了服務提供者框架的基礎,例如Java數據庫鏈接API(JDBC)。服務提供者框架是一個系統:多個服務提供者實現一個服務,系統為客戶端提供服務的多個實現,使客戶端與服務實現解耦。
服務提供者框架有三個基本的組件:服務接口,提供者實現;提供者注冊API,系統用來注冊實現,使客戶端能訪問它們;服務訪問API,客戶端用來得到服務實例。服務訪問API通常允許但不要求客戶端指定一些選擇提供者的規則。在沒有指定的情況下,API返回一個默認的實現實例。服務訪問API是"靈活的靜態工廠",其形成了服務提供者框架的基礎。
服務提供者框架的第四個可選組件是服務提供者接口,服務提供者通過實現這個接口來創建服務實現的實例。在沒有服務提供者接口的情況下,服務實現通過類名進行注冊,通過反射來進行實例化(Item 53)。在JDBC的案例中,Connection
是服務接口,DriverManager.registerDriver
是提供者注冊API,DriverManager.getConnection
服務訪問API,Driver
是服務提供者接口。
服務提供者框架模式有許多變種。例如,服務訪問API通過使用適配器模式[Gamma95, p. 139],能返回比提供者需要的更更豐富的服務接口。下面是服務提供者接口的一個簡單實現和默認的提供者:
// Service provider framework sketch
// Service interface
public interface Service {
... // Service-specific methods go here
}
// Service provider interface
public interface Provider {
Service newService();
}
// Noninstantiable class for service registration and access
public class Services {
private Services() { } // Prevents instantiation (Item 4)
// Maps service names to services
private static final Map<String, Provider> providers =
new ConcurrentHashMap<String, Provider>();
public static final String DEFAULT_PROVIDER_NAME = "<def>";
// Provider registration API
public static void registerDefaultProvider(Provider p) {
registerProvider(DEFAULT_PROVIDER_NAME, p);
}
public static void registerProvider(String name, Provider p){
providers.put(name, p);
}
// Service access API
public static Service newInstance() {
return newInstance(DEFAULT_PROVIDER_NAME);
}
public static Service newInstance(String name) {
Provider p = providers.get(name);
if (p == null)
throw new IllegalArgumentException(
"No provider registered with name: " + name);
return p.newService();
}
}
靜態工廠方法的第四個優勢是它們降低了創建參數化類型實例的冗長性。遺憾的是,當你調用參數化類的構造函數時,你必須指定類型參數,即使它們在上下文中是非常明顯的。這通常需要你緊接著提供兩次類型參數:
Map<String, List<String>> m =
new HashMap<String, List<String>>();
隨著類型參數長度和復雜性的增加,這個冗長的說明很快就讓人變得很痛苦。但是使用靜態工廠的話,編譯器可以為你找出類型參數。這被稱為類型推導。例如,假設HashMap
由這個靜態工廠提供:
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
你可以將上面冗長的聲明用下面簡潔的形式去替換:
Map<String, List<String>> m = HashMap.newInstance();
某一天,Java語言可能在構造函數調用上也有與方法調用類似的類型推導,但到發行版本1.6為止,它一直沒有。
遺憾的是,但到發行版本1.6為止,標準集合實現例如HashMap
沒有工廠方法,但你可以把這些方法放到你自己的工具類力。更重要的是,你可以在你自己的參數化類里提供這樣的靜態工廠。
只提供靜態工廠方法的缺點是沒有公有或保護構造函數的類不能進行子類化。公有靜態工廠返回的非公有類同樣如此。例如,不可能子類化集合框架中的這些便利實現類。可以說這是因禍得福,因為它鼓勵程序員使用組合來代替繼承(Item 16)。
靜態工廠方法的第二個缺點是它們不能很容易的與其它靜態方法進行區分。它們不能像構造函數那樣在API文檔中明確標識出來,因此很難弄明白怎樣實例化一個提供靜態工廠方法代替構造函數的類。Javadoc工具可能某一天會關注靜態工廠方法。同時,你可以通過在類中或接口注釋中注意靜態工廠和遵循通用命名約定來減少這個劣勢。下面是靜態工廠方法的一些常用命名:
valueOf
— 不嚴格地說,返回一個與它的參數值相同的一個實例。這種靜態工廠是有效的類型轉換方法。of
—valueOf
的一種簡潔替代方法,通過EnumSet
(Item 32)得到普及。getInstance
— 返回一個通過參數描述的實例,但不能說是相同的值。在單例情況下,getInstance
沒有參數并且返回唯一的一個實例。newInstance
— 除了newInstance
保證每個返回的實例都是與其它的實例不同之外,其它的類似于getInstance
,getType
— 類似于getInstance
,當靜態工廠方法在不同的類中時使用。Type
表示靜態工廠方法返回的對象類型。newType
— 類似于newInstance
,當靜態工廠方法在不同的類中時使用。Type
表示靜態工廠方法返回的對象類型。
總之,靜態工廠方法和公有構造函數都有它們的作用,理解它們的相對優勢是值得的。靜態工廠經常是更合適的,因此要避免習慣性的提供公有構造函數而不首先考慮靜態工廠。