Effective Java 3rd Edition -- Consider static factory methods instead of constructors

一個類允許客戶獲得一個實例的傳統方式是提供一個公有構造器。還有另一種技術也應該在每個程序員的工具箱種佔有一席之地。類可以提供一個公有的靜態工廠方法(static factory method),它只是一個用於返回類的靜態實例的方法。下面是一個來自 Boolean(基本類型 boolean 的封裝類)的簡單示例。這個方法將 boolean 基本類型轉換爲 Boolean 對象引用:

public static Boolean valueOf(boolean b){
    return b ? Boolean.TRUE : Boolean.FALSE;
}

注意,靜態工廠方法與設計模式[Gamma95]中的工廠方法模式不同。本條目中的這個靜態工廠方法並不直接對應於設計模式中的工廠方法。

類可以通過靜態工廠方法提供其客戶端,而不是通過構造器。提供靜態工廠方法而不是公共構造器有利也有弊。

靜態工廠方法的其中一個優勢在於,與構造器不同,它們有名稱。如果構造其的參數本身並不描述返回的對象,則具有適當名稱的靜態工廠更易於使用,產生的客戶端代碼更易於閱讀。例如,構造器 constructor BigInteger(int, int, Random) 返回的 BigInteger 可能爲素數,如果用名爲 BigInteger.probablePrime 的靜態工廠方法來表示,顯然更爲清楚。(這個方法在 Java 4 中被加入)

一個類只能有一個帶指定簽名的構造器。編程人員通常知道如何避開這一限制:通過提供兩個構造器,它們的參數列表旨在參數類型的順序上有所不同。實際上這並不是個好主意。面對這樣的 API,用戶永遠也記不住改用哪個構造器,結果常常會調用錯誤的構造器。並且,讀到使用了這樣構造器的代碼時,如果沒有參考類的文檔,往往不知所云。

由於靜態工廠方法有名稱,所以它們不受任何上述的限制。當一個類需要多個帶有相同簽名的構造器時,就用靜態工廠方法代替構造器,並且慎重地選擇名稱以便突出他們之間地區別。

靜態工廠方法的第二個優勢在於,與構造器不同,不需要每次調用時都創建一個新的對象。這就允許不可變類(條目 17)使用預構造的實例。或者吧已經構造好的實例緩存起來,以後再把這些實例分發給客戶,從而避免創建不必要的重複對象。Boolean.valueOf(boolean)方法說明了這項技術:它從來不創建對象。這項技術類似享元模式[Gamma 95]。如果一個程序要頻繁地創建相同的對象,並且創建對象的代價很高,則這項技術可以極大地提高性能。

靜態工廠方法可以重複的調用返回同一個對象,這u額可以被用來控制“在某一時刻那些實例應該存在”。這樣做的類被稱作是實例控制的。有幾個理由去寫這種實例控制的類。實例控制允許一個類保證它是一個單例(條目 3)或者不可實例化的(條目 4)。同樣,它也允許一個非可變類(條目 17)確保沒有兩個相同實例存在。當且僅當 a == b 的時候纔有 a.equals(b)。這是享元模式的基礎[Gamma 95]。枚舉類型(條目 34)提供這種保證。

靜態工廠方法的第二個優勢在於,不像構造器,它們可以返回一個元返回類型的子類的對象。這樣我們在選擇被返回對象的類型時就有了更大的靈活性。

這種靈活性的一個應用是,一個 API 可以返回一個對象,同時又不使該對象的類型稱爲共有的。以這種方式把具體的實現類隱藏起來,可以得到一個非常簡潔的 API。這項技術非常適用於基於接口的框架(條目 20),因爲在這樣的框架結構中,接口稱爲靜態工廠方法的自然返回實例。

在 Java 8 之前,接口不能有靜態方法。按照慣例,名爲 Type 的接口的靜態工廠方法被放入一個名爲 Types 的不可實現的伴隨類(條目 4)中。例如:Java
Collections Framework 有 45 個幾口的實用程序實現,提供了非可修改集合、同步集合等。這些實現絕大多數都是通過一個不可實例化的類(java.util.Collections)中的靜態工廠方法而被導出的。返回的對象的類都是非公有的。

Collections Framework API 比導出 45個獨立的公有類要小得多,這不僅僅是指 API 數量上的減少,而且也是概念意義上的減少:編程人員爲了使用 API 必須掌握的概念的數量和難度。編程人員知道,被返回的對象是由相關接口精確指定的,所以它們不需要閱讀有關實現類的類文檔。更進一步,使用這樣的精要工廠方法,可以強迫客戶通過接口來引用被返回的對象,而不是通過實現類來引用被返回的對象,這是一個很好的習慣(條目 64)。

從 Java 8 開始,接口不能包含靜態方法的限制被消除了,所以通常沒有理由爲接口提供不可實例化類的伴隨類。很多公開的靜態成員應該放在這個接口本身中。但是,請注意,人有必要將這些靜態方法背後的大部分實現代碼放在單獨的包-私有類中。這是因爲 Java 8 要求所有接口的靜態成員都是公開的。Java 9 允許私有靜態方法,但靜態字段和靜態成員仍然需要公開。

靜態工廠的第四個優點是,返回對象的類可以根據輸入參數的不同而不同。聲明的返回來行的任何子類型都是允許的。返回對象的類也可能引發不版本而異。

EnumSet 類(條目 36)沒有公有構造函數,只有靜態工廠。在 OpenJDK 視線中,它們根據基本美劇類型的大小返回兩個子類之一的實例:如果它有 64 個或少量元素
The EnumSet class (Item 36) has no public constructors, only static factories.In the OpenJDK implementation, they return an instance of one of two subclasses,depending on the size of the underlying enum type: if it has sixty-four or few erelements, as most enum types do, the static factories return a RegularEnum Setinstance, which is backed by a single long; if the enum type has sixty-five or more elements, the factories return a JumboEnumSet instance, backed by a long array.

這兩個實現類的存在對客戶是不可見的。如果 RegularEnumSet 不再爲小型枚舉類型提供性能優勢,則可以在未來版本中將其淘汰,而不會產生不良影響。

同樣,未來的版本可能會添加 EnumSet 的第三個或第四個實現,如果它證明對性能有益。客戶及不知道也不關心它們從工廠回來的對象的類;它們只關心它是 EnumSet 的子類。

靜態工廠的第五個優點是,當包含方法的類被寫入時,返回對象的類不需要存在。這種靈活的靜態工廠方法構成了服務供應商框架的基礎,如 Java Database Connectivity API(JDBC)。服務提供者框架時供應商實現服務的系統,並且系統使得實現對客戶端可用,從而將客戶端與實現解耦。

服務供應商框架中有三個基本組件:一個代表實現的服務接口;供應商註冊 API,供應商用於註冊實施;和一個服務訪問 API,客戶用它來獲取服務的實例。服務訪問 API 可以允許客戶指定選擇實現的標準。在沒有這樣的標準的情況下,API 返回默認實現的實例,或者允許客戶循環遍歷所有可用的實例。服務訪問 API 時構成服務提供商框架基礎的靈活的靜態工廠。

服務提供商框架的可選第四個組件時服務提供商接口,它描述了生成服務接口實例的工廠對象。在沒有服務提供商接口的情況下,實現必須反射性(條目 65)地實例化。在 JDBC 的情況下,Connection 扮演服務接口的一部分,DriverManager.registerDriver 時提供商註冊 API,DiverManager.getConnection 是服務訪問 API,Driver 是服務提供商接口。

服務提供商框架模式有許多變種。例如:服務訪問 API 可以向客戶端返回比提供商提供的更豐富的服務接口。這就是橋樑模式[Gamma 95]。依賴注入框架(條目 5)可以被視爲強大的服務提供者。自 Java 6 依賴,該平臺包含一個通用服務提供商框架 java.util.ServiceLoader,因此你不需要,一般也不應編寫自己的(條目 59)。JDBC 不是用 ServiceLoader,因爲前者早於後者。

僅提供靜態工廠方法的主要限制是沒有公有或受保護的構造函數的類不能被子類化。例如,不可能在集合框架中對任何便利實現類進行子類化。可以說,這可能是一個僞裝的祝福,因爲它鼓勵程序員使用組合而不是繼承(條目 18),並且對非可變類型(條目 17)是必須的。

靜態工廠方法的第二個缺點是程序員很難找到它們。它們並不像構造函數那樣在 API 文檔中脫穎而出,因此很難弄清楚如何實例化一個提供靜態工廠方法而不是構造函數的類。Javadoc 工具有一天會吸引人們關注靜態工廠方法。與此同時,你可以通過在類或接口文檔中引起對靜態工廠的注意並遵守通用命名約定來減少此問題。以下是靜態工廠方法的一些常用名稱。這份清單並非詳盡無遺:

  • from—類型轉換方法,它接收單個參數並返回此類型的相應實例,例如:Date d = Date.from(instant);
  • of—一個接收多個參數並返回包含它們的此類型實例的聚合方法,例如:Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf—比 from 和 of 更爲詳盡的替代方法,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance or getInstance—返回由其參數描述的實例(如果有),但不能說具體有相同的值,例如:StackWalker luke = StackWalker.getInstance(options);
  • create or newInstance—就像 instance 和 getInstance,但該方法保證每個調用都返回一個新的實例,例如:Object newArray = Array.newInstance(classObject, arrayLen);
  • getType—就像 getInstance,但在工廠方法位於不同類中使用。Type 是工廠方法返回的對象的類型,例如:FileStore fs = Files.getFileStore(path);
  • newType—就像 newInstance,但在工廠方法位於不用類中使用。Type 是工廠方法返回的對象的類型,例如:BufferedReader br = Files.newBufferedReader(path);
  • type—getType 和 newType 的簡潔替代方法,例如:List<Complaint> litany = Collections.list(legacyLitany);

總之,靜態工廠方法和公有構造函數都有它們的用途,並且瞭解它們的相對優點是值得的。通常情況下,靜態工廠是可取的,所以避免反思,在沒有首先考慮靜態工廠的情況下提供公共構造函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章