effective java 3th item1:考慮靜態工廠方法代替構造器

  • 2019 年 10 月 3 日
  • 筆記

傳統的方式獲取一個類的實例,是通過提供一個 public 構造器。這裡有技巧,每一個程序員應該記住。一個類可以對外提供一個 public靜態工廠方法 ,該方法只是一個樸素的靜態方法,不需要有太多複雜的邏輯,只需要返回該類的實例。

這裡通過 Boolean (是原始類型 boolean 的包裝類)舉一個簡單的例子:

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

這個方法,將一個 boolean 原始類型的值轉換為 Boolean 對象引用。

值得注意的是,本條目中說的一個 靜態工廠方法 不同於 設計模式 的工廠模式1,同樣的,本條目中描述的靜態工廠方法在設計模式找不到對應的模式。

一個類可以對外提供靜態工廠方法,來取代 public 的構造器,或者與 public 構造器並存,對外提供兩種方式獲取實例。用靜態工廠方法取代 public 構造器,既有優勢也有缺點。

優勢體現在下面幾點:

  1. 靜態工廠方法與構造器比起來,它可以隨意命名,而非固定的與類的名字保持一致。

    如果一個構造器的參數本身,不能對將要返回的對象具有準確的描述。此時使用一個具有準確描述名字的靜態工廠方法是一個不錯的選擇。它可以通過名字對將要返回的對象,進行準確的描述。使得使用的人可以見名知意。

    舉個例子,BigInteger(int, int, Random) 構造器,返回的值可能是素數,因此,這裡其實可以有更好的表達,通過使用一個靜態工廠方法 BigInteger.probablePrime(int, int, Random) 該方法於 1.4 被加入。

    一個類只能有一個指定方法簽名2的構造器。通常我們都知道如何繞過這限制,通過交換參數列表的順序,得到不同的方法簽名。但是這是一個很糟糕的主意。這給使用 api 的開發人員造成負擔,他們將很難記住哪一個方法簽名對應哪一個對象的返回,最後往往都是錯誤的調用。閱讀代碼的人同樣也蒙圈,如果沒有相應的文檔告訴他們,不同的方法簽名對應的構造器返回的對象是什麼。

    靜態工廠方法不受上述限制,不需要去通過交換參數順序來彼此區分,因為它們可以擁有自己的名字。因此,當一個類的多個構造器,方法簽名差不多,僅僅參數順序不一樣的時候,考慮使用靜態工廠方法,仔細的為靜態工廠方法取名字,以區分它們之間的不同。

  2. 靜態工廠方法與構造器比起來,不必每次調用都創建新的對象

    這允許不可變的類使用預創建的實例3,或者在創建實例的時候,將實例緩存起來4,重複的使用該實例,避免創建不重要的重複對象。Boolean.valueOf(boolean) 方法使用該技巧,它永遠都不會創建對象,返回的都是預創建好的對象。這個技巧有點類似於設計模式中的享元模式5 。它能大幅度的提高性能,特別是在特定場景下:一些對象創建的時候,需要花費很大性能,並且這些對象經常被使用。

    該特性允許類在任何時候,對其產生多少實例具有精確的控制。用這種技巧的類,被稱為實例受控的類。這裡有幾個使用實例受控類的理由。實例受控允許一個類保證它是一個單例或者不可實例化的類。同樣的,實例受控,也可以保證不可變類不會存在兩個相等的實例。

  3. 靜態工廠方法與構造器比起來,可以返回該類的任意子類型的對象

    具有足夠的靈活性,在獲取對象的時候。可以返回協變類型,在方法中使用該類的子類構造器創建對象,然後返回,同時對外不需要暴露這些子類對象,適合於面向接口編程。在 1.8 之前,接口中不能有靜態方法,針對情況的慣例做法是,針對名為 Type 類型的接口,它的靜態方法被放在一個不可實例化的類 Types6,典型的例子是 java.util.Collections 類,它通過靜態工廠方法,可以構建返回各式各樣的集合:同步集合、不可修改的集合等等,但是返回的時候都是返回接口類型,具體實現類型不對外公開。

    // Collections 中非公開類,同步map     private static class SynchronizedMap<K,V>          implements Map<K,V>, Serializable {          private static final long serialVersionUID = 1978198479659022715L;            private final Map<K,V> m;     // Backing Map          final Object      mutex;        // Object on which to synchronize            SynchronizedMap(Map<K,V> m) {              this.m = Objects.requireNonNull(m);              mutex = this;          }            SynchronizedMap(Map<K,V> m, Object mutex) {              this.m = m;              this.mutex = mutex;          }        // Collections 的靜態工廠方法,返回接口接口map,但是內部是返回同步Map類型。     public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {          return new SynchronizedMap<>(m);      }

    起到簡化 api 的作用,這種簡化不僅僅是 api 體積上的減少,減少對外暴露的類的數量,也給程序員直觀上的簡潔,他們只需要記住接口類型即可,無需記住具體的實現類型。這樣也督促程序員面向接口編程,這是一種好的習慣。

    1.8 以後,接口可以寫靜態方法。因此,不再需要按照以前的習慣,為接口,寫一個對應的類,直接在接口中寫靜態工廠方法。但是關於返回的實現類型,依然應該繼續隱藏,使用非公開類實現。

  4. 靜態工廠方法與構造器比起來,可以隨着傳入參數的不同,返回不同的對象。

    可以返回任何子類型,和第三條一樣,但是可以繼續添加控制,根據傳入參數的不同,返回不同的對象。

    一個例子,EnumSet ,是一個不可實例的類,只有一個靜態工廠方法,沒有構造器。但是在 openJDK 的實現中,具體的返回類型,是根據實際枚舉的個數決定的,如果小於等於 64,則返回 RegularEnumSet 類型,否則返回 JumboEnumSet 類型。這兩個類型對於使用者來說,都是不可見的。如果 RegularEnumSet 類型,在將來不再為小的枚舉類型提供優勢,即便在未來的發行版刪除 RegularEnumSet 也不會有什麼影響。同樣的,未來也可以繼續添加第三個、第四個版本,對使用者也是無感的。使用者不需要關心具體返回的是什麼對象,他們只知道,返回的對象都是 EnumSet 類型。

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {          Enum<?>[] universe = getUniverse(elementType);          if (universe == null)              throw new ClassCastException(elementType + " not an enum");            // 如果小於等於 64 ,則返回 RegularEnumSet          if (universe.length <= 64)              return new RegularEnumSet<>(elementType, universe);          else              return new JumboEnumSet<>(elementType, universe);      }        // RegularEnumSet 類是 EnumSet的子類      class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {            ...      }    
  5. 靜態工廠方法與構造器比起來,在編寫靜態工廠方法的時候,具體的類型可以不存在。

    這句話還是面向接口的優勢,意思就是,我們在編寫方法的時候,可以沒有任何實現類,在使用的使用,先註冊實現類,然後再返回實現類,這使得擴展變得很容易。我們只是在維護一個框架,一個接口,具體的實現,我們不給出,誰都可以實現,然後註冊使用。這也是 服務者框架 的含義。JDBC 就是這麼一個思想的服務者框架。關於服務者框架看這裡

缺點:

  1. 靜態工廠方法與構造器比起來,沒有 public 或者 protected 修飾的構造器,無法實現繼承

    例如,上面提到的 Collections ,就無法被繼承,我們就不能繼承其中的任何一個便利的實現。這或許,也是一種對使用組合而非繼承的鼓勵。

  2. 靜態工廠方法與構造器比起來,它們不容易被程序員所知曉

    在文檔中,它們不像構造器那麼顯眼,在上面單獨的列出來,基於這個原因,要想知道如何實例化一個類,使用靜態工廠方法比使用構造器相比,前者是比較困難的,因為文檔中,靜態工廠方法和其他靜態方法沒啥區別,沒有做特殊處理。java 文檔工具或許在未來會注意到這個問題,對靜態工廠方法多給予一些關注。

    同時,我們可以在文檔中對靜態工廠方法的名字做一些特殊處理,遵守常見的命名規範,來減少這個問題,比如像下面提到的幾個規範:

    1. from類型轉換方法,根據一個單一傳入的參數,返回一個對應的類型,比如:Date d = Date.from(instant);
    2. of聚合方法,接受多個參數,返回一個合併它們的實例。比如:Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
    3. valueOf比 from 和 of 更加詳細 。比如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
    4. instance or getInstance獲取實例方法,根據傳入的參數,返回實例,但是不保證返回的實例完全一樣,根據傳入的參數不同而不同。比如:StackWalker luke = StackWalker.getInstance(options);
    5. create or newInstance獲取新的實例,每次都返回新創建的實例。比如:Object newArray = Array.newInstance(classObject, arrayLen);
    6. getType ,和 getInstance 類似,用於返回的類型,不是靜態工廠方法所在的類,而是其他類型。比如:FileStore fs = Files.getFileStore(path);
    7. newTypenewInstance 類型,同樣用於返回的類型,不是靜態工廠方法所在的類,而是其他類型。比如:BufferedReader br = Files.newBufferedReader(path);
    8. typegetType and newType 的簡化版。比如:List<Complaint> litany = Collections.list(legacyLitany);

總結下,靜態工廠方法和 public 構造器都有自己的優點,了解它們各自的優點是有幫助的。大部分情況下,靜態工廠方法更佔優勢,所以,我們應該避免第一反應就使用構造器,而是先考慮下靜態工廠方法。



  1. 這裡的靜態工廠方法,和設計模式中的靜態工廠模式,很相似,但是設計模式中的靜態工廠模式,它是對外隱藏對象的實現細節,通過一個工廠,根據不同的輸入,產生不同的輸出。本條目中的工廠,只會產生特定的輸出,即自己的實例。二者還是不同的。

  2. 方法簽名,指的是方法名字,以及方法參數列表,包括方法參數的順序。

  3. 類似於單例模式的餓漢式

  4. 類似於單例模式的懶漢式

  5. 享元模式,23種設計模式中的一種, 它針對每一種內部狀態僅提供一個對象,設置不同的外部狀態,產生多種不同的形態,但是其內部狀態對象只有一個,達到對象復用的目的。

  6. 這裡的 Tyle 類型,不是泛型的 Type ,只是一種代指,跟 xxx 一個意思,表示 1.7 以前,對於面向接口編程的時候,想要返回協變類型,常規的做法,是寫一個不可實現類 ,類的名字,就是接口的名字,多加一個 s