考慮使用靜態工廠方法代替構造器 類可以提供一個公有的靜態工廠方法(public static factory method)來返回一個類的實例。例如,Boolean類的valueOf()方法: public static Boolean valueOf(boolean b) { return (b ...
考慮使用靜態工廠方法代替構造器
類可以提供一個公有的靜態工廠方法(public static factory method)來返回一個類的實例。
例如,Boolean類的valueOf()方法:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
提供public靜態工廠方法而不是public的構造器的優勢如下:
-
靜態工廠方法有名稱,不過要慎重地選擇名稱
例如,構造器BigIntger(int, int, Random)返回的BigInteger可能為素數,如果用名為BigInteger.probablePrime()的靜態工廠方法顯然更清晰。
示例: BigIntegerTest -
不必在每次調用靜態工廠方法的時候都創建一個新對象
如果程式經常經常請求創建相同的對象,並且創建對象的代碼很高,則這項技術可以極大地提升性能。
示例:BooleanTest
當且僅當a==b的時候才有a.equals(b)為true, 如果類保證了這一點, 它的客戶端就可以使用==操作符來代替equals(Object)方法,這樣可以提升性能。 -
靜態工廠方法可以返回子類類型的對象
API返回的對象對應的子類可以不是公有的,這種方式可以隱藏具體的實現類。
這項技術適合於基於介面的框架,介面為靜態工廠提供了自然返回類型。
介面不能有靜態方法,因此按照慣例,返回介面的靜態工廠方法是放在一個不可實例化(non-instantiability)的類中。
示例: InterfaceCannotStaticTest
示例: java.util.Collections是一個不可實例化的類,它提供了不可修改的集合、同步集合的靜態工廠方法。我們模仿其寫了一個簡單例子: MyListTest
通過使用這種靜態工廠方法,甚至要求客戶端通過介面來引用被返回的對象,而不是通過它的實現類來引用被返回的對象,這是一種良好的習慣。 為了提升軟體的可維護性和性能,返回對象的(子)類也可能隨著發行版本的不同而不同。
示例:EnumSetTest 發行版本1.5中引入java.util.EnumSet沒有公有的構造器,只有靜態工廠方法,返回兩種實現類之一,具體則取決於底層枚舉類型的大小。
示例:服務提供者框架 -
靜態工廠方法在創建參數化類型實例的時候,使得代碼更加簡潔
例如,創建一個Map<String, List<String>>
對象,在調用參數化類的構造器時,即使類型參數明顯,也必須指明。這通常要求你連接兩次提供類型參數:Map<String, List<String>> map = new HashMap<String, List<String>>();
隨著類型參數變得越來越長,越來越複雜,這一冗長的說明也很快變得痛苦起來。
但是有了靜態方法,編譯器可以替你找到類型參數,這被稱作類型推導(type inference):Map<String, List<String>> map = HashMap.newInstance();
假設HashMap提供了這個靜態工廠方法:public static <K, V> HashMap<K, V> newInstance() { return new HashMap<K, V>(); }
總有一天,Java將能夠在構造器調用以及方法調用中執行這些類型推導,但到發行版本1.6為止暫時還無法這麼做。
遺憾的是,到發行版本1.6為止,標準的集合實現如HashMap並沒有工廠方法,但是可以把這些方法放在你自己的工具類中。
公有的靜態工廠方法的缺點:
- 類如果不含有公有的或者受保護的構造器,靜態工廠方法返回的實例不能被子類化。
- 同樣地,對於非公有類(nonpublic classes),靜態工廠方法返回的實例也不能被子類化。 示例: MyListTest
- 靜態工廠方法與其它的靜態方法無法被明確區分,不像構造器那樣在api文檔中被明確的標識出來。 所以,在類或介面註釋中關註靜態工廠,使用一些約定俗成的命名
慣用名稱 | 說明 |
---|---|
valueOf | Returns an instance that has, loosely speaking, the same value as its parameters. Such static factories are effectively type-conversion methods. |
of | A concise alternative to valueOf , popularized by EnumSet. |
getInstance | Returns an instance that is described by the parameters but cannot be said to have the same value. In the case of a singleton, getInstance takes no parameters and returns the sole instance. |
newInstance | Like getInstance , except that newInstance guarantees that each instance returned is distinct from all others. |
getType | Like getInstance , but used when the factory method is in a different class. Type indicates the type of object returned by the factory method. |
newType | Like newInstance , but used when the factory method is in a different class. Type indicates the type of object returned by the factory method. |
簡而言之,靜態工廠方法和公有構造器都各有用處。 靜態工廠方法通常更加適合,因此切忌第一反應是提供公有的構造器,而不先考慮靜態工廠方法。
遇到多個構造器參數時要考慮用構建器
靜態工廠和構造器有一個共同的局限性:它們都不能很好地擴展到大量的可選參數。考慮用一個類表示一個食品的營養成分表。對於這樣的類,應該用哪個構造器或者靜態方法來編寫呢?
- 重疊構造器(telescoping constructor)
示例: telescoping_constructor/NutritionFacts
① 當有許多參數的時候,客戶端代碼會很難編寫,並且仍然較難以閱讀。
② 如果讀者想知道那些值是什麼意思,必須很仔細地數著這些參數來探個究竟。如果客戶端不小心顛倒了其中兩個參數的順序,編譯器也不會出錯,但是在程式運行時會出現錯誤行為。 - JavaBeans模式:調用一個無參構造器來創建對象,然後調用setter()方法來設置每個必要的參數,以及每個相關的可選參數。
示例: javabeans/NutritionFacts
① 無法保證一致性:JavaBean模式自身有著很嚴重的缺點,因為構造過程被分到了幾個調用中,在構造過程中JavaBean可能處在不一致的狀態。類無法僅僅通過檢驗構造器參數的有效性來保證一致性。試圖使用處於不一致狀態的對象,將會導致失敗,這種失敗調試起來十分困難。
② 阻止了把類做成不可變,這就需要程式員付出額外的努力來確保它的線程安全。 - Builder模式
示例:builder/NutritionFacts
這樣的客戶端代碼很容易編寫,而且易讀。Builder模式模擬了具名的可選參數,就像Python一樣。
對參數加強約束條件: build()方法檢驗或者多個setter()方法檢驗這些約束條件,如果該約束條件沒有得到滿足,就拋出IllegalStateException或IllegalArgumentException。
設置了參數的Builder生成了一個很好的抽象工廠(Abstract Factory)。Java中的Class對象就是一個抽象工廠,用newInstance()方法充當build()方法的一部分。
Builder模式也有它自身不足,為了創建對象,必須先創建它的構建器,它還會比重疊構造器模式更加冗長,因此只有在很多參數(4個以上)的時候才考慮使用Builder模式。但是要記住,將來你可能需要添加參數,如果一開始就使用構造器或者靜態工廠,等到類需要多個參數時才添加構建器,就無法控制,那些過時的構造器或者靜態工廠顯得十分不協調。因此通常最好一開始就使用構建器。
用私有構造器或者枚舉類型強化Singleton屬性
Singleton指僅僅被實例化一次的類。Singleton通常被用來代表那些本質上唯一的系統組件。
在Java 1.5發行版本之前,實現Singleton有兩種方法。這兩種方法都要把構造器保持為私有的,並導出公有的靜態成員,以便允許客戶端能夠訪問該類的唯一實例。
第一種方法,公有靜態成員是一個final域。示例: field/Elvis.java
私有構造器僅被調用一次,用來實例化公有的靜態域Elvis.INSTANCE。
但要提醒一點:享有特權的客戶端可以藉助AccessibleObject.setAccessible()方法,通過反射機制調用私有構造器,示例:ModifyingSingleton 。如果需要抵禦這種攻擊,可以修改構造器,讓它在被要求創建第二個實例的時候拋出異常。
通過私有構造器強化不可實例化的能力
避免創建不必要的對象
消除過期的對象引用
避免使用終結方法
參考資料
- 《Effective Java》第2版
- Java反射AccessibleObject類的setAccessible方法