1. 程式人生 > >effective java 3th item1:考慮靜態工廠方法代替構造器

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

傳統的方式獲取一個類的例項,是通過提供一個 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。↩