Effective Java - 靜態方法與構造器

c旋兒發表於2019-07-08

用靜態工廠方法替代構造器?

傳統來講,為了使客戶端能夠獲取它自身的一個例項,最傳統的方法就是提供一個公有的構造器。像下面這樣

public class Apple {

    public Apple(){}

    public static void main(String[] args) {
        Apple apple = new Apple();
    }
}

還有另外一種方式,為類提供靜態工廠方法,它只是返回一個類的靜態方法,下面是它的構造

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

上面程式碼定義了一個valueof(boolean b)的靜態方法,此方法的返回值是一個對常量的的引用,為什麼說是常量?跟蹤程式碼進去發現,TRUE是使用static final 修飾的。Boolean.TRUE 實際指向的就是一個Boolean類的帶有boolean型別建構函式。

public static final Boolean TRUE = new Boolean(true);

注意:此靜態工廠方法與設計模式中的工廠方法模式不同,本條目中所指的靜態方法並不直接對應設計模式中的工廠方法。

那麼我們為蘋果增加一個屬性appleSize,並分別提供靜態的建構函式bigApple和smallApple,並提供一個方法來判斷傳進來的值,如果appleSize > 5的話就是大蘋果,否則都是小蘋果,改造後的程式碼如下

public class Apple {

    static int appleSize;
    public static final Apple bigApple = new Apple(5);
    public static final Apple smallApple = new Apple(2);

    public Apple(){}
    public Apple(int appleSize){
        this.appleSize = appleSize;
    }
}

public class testApple {

    // 判斷蘋果的大小,大於5的都按5斤算,小於5的都按2斤算
    static Apple judgeAppleSize(int size){
        return size > 5 ? Apple.bigApple : Apple.smallApple;
    }
    public static void main(String[] args) {
//        Apple apple = new Apple();
        judgeAppleSize(6);
    }
}

那麼,你能否根據上述兩個程式碼思考一下靜態工廠方法和公有構造器之間孰優孰劣呢?

靜態工廠有名稱

眾所周知,構造器的宣告必須與類名相同,構造方法顧名思義就是構造此類的方法,也就是通過構造方法能夠獲得這個類物件的引用,所以構造方法必須與類名相同。不知道你有沒有遇見過類似的情況,看下面一個例子

BigInteger.java

public BigInteger(int bitLength, int certainty, Random rnd) {
  ...
  prime = (bitLength < SMALL_PRIME_THRESHOLD
                                ? smallPrime(bitLength, certainty, rnd)
                                : largePrime(bitLength, certainty, rnd));
}

如果只是給BigInteger 傳遞了三個引數,但是你並不知道它的內部程式碼是怎樣的,你可能還會查詢到對應的原始碼來仔細研究,也就是說BigInteger 的名稱和內部實現沒有太大的關係。

如果用靜態工廠方法呢?可以看下面一個例子

還是BigInteger.java

public static BigInteger probablePrime(int bitLength, Random rnd) {
  if (bitLength < 2)
    throw new ArithmeticException("bitLength < 2");

  return (bitLength < SMALL_PRIME_THRESHOLD ?
          smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
          largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}

private static BigInteger smallPrime(int bitLength, int certainty, Random rnd) {...}
private static BigInteger largePrime(int bitLength, int certainty, Random rnd) {...}

同樣是內部呼叫,靜態工廠方法probablePrime是你自己定義的名稱,你是否從該名稱看出來某些關於內部實現的東西呢?是不是就比呼叫其公有的建構函式要更加明確?

一個類只能有一個帶有指定簽名的構造器,如果提供兩個構造器,他們只是在引數型別的順序上有所不同,你是不是也會有一頭霧水不知道該呼叫哪個構造器的感覺?事實上這並不是一個好的注意,面對這樣的API,使用者也記不住呼叫哪個構造器,結果通常會呼叫錯誤的構造器。

由於靜態方法有名稱,所以在實現過程中,所以它們不受上述限制,當一個類需要多個帶有相同簽名的構造器時,就用靜態工廠方法替代構造器,並仔細的選取靜態工廠的名稱以便突出其主要功能。

靜態工廠不必重新建立一個物件

我們都知道,每一次呼叫一個建構函式都相當於是重新建立了一個該物件的例項,這使得不可變類可以使用預先構建好的示例,或者將構建好的例項快取起來,重複利用,從而避免建立不必要的物件。Boolean.valueOf(boolean)方法說明了這一點,它從來不用建立物件,這種方法類似於享元模式,簡單介紹一下:

享元模式

https://www.runoob.com/design-pattern/flyweight-pattern.html

言歸正傳,靜態工廠方法不會重新建立物件,靜態工廠方法每次都返回相同的物件,這樣有助於控制哪些類的例項應該存在。這種類稱為例項受控的類,我們以單例模式為例,來看一下例項受控的類的主要用法:

public class Singleton {

    // 懶漢式
    private static Singleton INSTANCE;
    private Singleton(){}
    public static Singleton newInstance(){
        if(INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

這部分程式碼是一個典型的懶漢式實現,對外部只開放newInstance方法,並把建構函式私有化,也就是說你不能通過建構函式new出Singleton的例項,必須通過Singleton.newInstance()來建立Singleton的例項,每次判斷INSTANCE是否為null,如果是null,則建立並返回 new Singleton()的引用,否則,只是返回之前建立出來的Singleton 的引用。

這個Singleton類,就是例項受控的類,你不能無限制的建立Singletion的例項,因為Singleton是一種單例實現。當然,這種方式不是執行緒安全的,在多個執行緒併發訪問時,你並不能保證單例的有效性,也就是說在多執行緒環境下你不能保證Singleton只有一個。那麼如何保證呢?請往下讀,下文會給你答案。

例項受控的類

編寫例項受控的類有幾個原因:

  1. 例項受控的類確保類是一個Singleton

Singleton是指僅僅被例項化一次的類。那麼如何編寫一個安全的Singleton呢?我們來對上面的懶漢式進行部分改造

public class Singleton {

    // 餓漢式
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
        return INSTANCE;
    }

}

使用static final強制了INSTANCE的引用物件為不可更改的,也就是說,你不能再把INSTANCE物件的引用指向其他new Singleton()物件,這種方式就是在類裝載的時候就完成例項化。避免了執行緒同步問題(其他單例的情況我們在後面的章節中討論)。

  1. 例項受控的類確保類是不能被例項化的

其實我們上面的程式碼一直在確保此規定,那就是通過私有化建構函式,確保此類不能被例項化。你也可以通過使用下面這種方式來避免類的例項化

public class UtilityClass {
  private UtilityClass(){
    throw new AssertionError();
  }
}

AssertionError()不是必須的,但是它可以避免不小心在類的內部呼叫構造器。

  1. 例項受控的類確保不會存在兩個相等的例項

例項受控的類確保不會存在兩個相等的例項,當且僅當 a==b時,a.equals(b)才為true,這是享元模式的基礎(具體我們在後面的章節中討論)。

靜態工廠可以返回任何子型別物件

靜態工廠方法與構造器不同的第三大優勢在於,它們可以返回原返回型別的任何子型別的物件。這樣我們就在選擇返回物件的類時就有了更大的靈活性。CollectionsArrays工具類保證了這一點

Collections.java

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {
  return new UnmodifiableCollection<>(c);
}

static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
  ...
    UnmodifiableCollection(Collection<? extends E> c) {
    if (c==null)
      throw new NullPointerException();
    this.c = c;
  } 
  ...
}

這是Collections.java 中的程式碼片段,靜態方法unmodifiableCollection返回一個新的UnmodifiableCollection,呼叫它的靜態方法建立UnmodifiableCollection的物件,由於UnmodifiableCollection繼承於Collection,也就是說靜態方法unmodifiableCollection其實是返回了一個子類的物件。

靜態工廠返回的類可以動態變化

靜態工廠的第四大優勢在於,所返回的物件的類可以隨著每次呼叫而發生變化,這取決於靜態工廠方法的引數值。只要是已宣告的返回型別的子型別,都是允許的。返回物件的類也可能隨著發行版本的不同而不同。

EnumSet (詳見第36條)沒有公有的構造器,只有靜態工廠方法。在OpenJdk實現中,它們返回兩種子類之一的一個例項,具體則取決於底層列舉型別的大小:如果它的元素有6 4個或者更少,就像大多數列舉型別一樣,靜態工廠方法就會返回一個RegularEnumSet例項,用單個long進行支援;如果列舉型別有65個或者更多元素,工廠就返回JumboEnumSet例項,用一個long陣列進行支援。

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");

  if (universe.length <= 64)
    return new RegularEnumSet<>(elementType, universe);
  else
    return new JumboEnumSet<>(elementType, universe);
}

靜態工廠返回的類可以不存在

靜態工廠的第五大優勢在於,方法返回物件所屬的類,在編寫包含該靜態工廠方法類時可以不存在。

這裡直接從 這種靜態工廠方法最典型的實現--服務提供者框架 來探討。

服務提供者框架包含四大元件:(概念不太好理解,可以直接先看下面的例子講解,然後回過頭來再看概念)

服務介面:這是服務提供者要去實現的介面
服務提供者介面:生成服務介面例項的工廠物件(就是用來生成服務介面的)(可選)
提供者註冊API:服務者 提供服務者自身的實現
服務訪問API:根據客戶端指定的某種條件去實現對應的服務提供者

//四大組成之一:服務介面
public interface LoginService {//這是一個登入服務
    public void login();
}
 
//四大組成之二:服務提供者介面
public interface Provider {//登入服務的提供者。通俗點說就是:通過這個newLoginService()可以獲得一個服務。
    public LoginService newLoginService();
}
 
/**
 * 這是一個服務管理器,裡面包含了四大組成中的三和四
 * 解釋:通過註冊將 服務提供者 加入map,然後通過一個靜態工廠方法 getService(String name) 返回不同的服務。
 */
public class ServiceManager {
    private static final Map<String, Provider> providers = new HashMap<String, Provider>();//map,儲存了註冊的服務
 
    private ServiceManager() {
    }
 
    //四大組成之三:提供者註冊API  (其實很簡單,就是註冊一下服務提供者)
    public static void registerProvider(String name, Provider provider) {
        providers.put(name, provider);
    }
 
    //四大組成之四:服務訪問API   (客戶端只需要傳遞一個name引數,系統會去匹配服務提供者,然後提供服務)  (靜態工廠方法)
    public static LoginService getService(String name) {
        Provider provider = providers.get(name);
        if (provider == null) {
            throw new IllegalArgumentException("No provider registered with name=" + name);
 
        }
        return provider.newLoginService();
    }
}

也可以參考這篇文章進一步理解:JAVA 服務提供者框架介紹

靜態工廠方法的缺點

靜態工廠方法依賴於建構函式的建立

上面提到了一些靜態工廠方法的優點,那麼任何事情都有利弊,靜態工廠方法主要缺點在於,類如果不含公有的或者受保護的構造器,就不能被子類化。例如,要想將Collections Framework中任何便利的實現類子類化,這是不可能的。

靜態工廠方法最終也是呼叫該類的構造方法,如果沒有該類的構造方法,靜態工廠的方法也就沒有意義,也就是說,靜態工廠方法其實是構造方法的一層封裝和外觀,其實最終還是呼叫的類的構造方法。

靜態工廠方法很難被發現

在API文件中,它們沒有像構造器那樣在API文件中被標明,因此,對於提供了靜態工廠方法而不是構造器的類來說,要想查明如何例項化一個類是非常困難的。下面提供了一些靜態工廠方法的慣用名稱。這裡只列出來了其中的一小部分

  • from ——— 型別轉換方法,它只有單個引數,返回該型別的一個相對應的例項,例如:
Date d = Date.form(instant);
  • of ——— 聚合方法,帶有多個引數,返回該型別的一個例項,把他們結合起來,例如:
Set<Rank> faceCards = EnumSet.of(JACK,QUEEN,KING);
  • valueOf ——— 比from 和 of 更繁瑣的一種替代方法,例如:
BigInteger prime = BigInteger.valueof(Integer.MAX_VALUE);
  • instance 或者 getInstance ———返回的例項是通過方法的(如有)引數來描述的,但是不能說與引數具有相同的值,例如:
StackWalker luke = StackWalker.getInstance(options);
  • create 或者 newInstance ——— 像instance 或者 getInstance 一樣,但create 或者 newInstance 能夠確保每次呼叫都返回一個新的例項,例如:
Object newArray = Array.newInstance(classObject,arrayLen);
  • getType ——— 像getInstance 一樣,但是在工廠方法處於不同的類中的時候使用。Type 表示工廠方法所返回的物件型別,例如:
FileStore fs = Files.getFileStore(path);
  • newType ——— 像newInstanfe 一樣,但是在工廠方法處於不用的類中的時候使用,Type表示工廠方法返回的物件型別,例如:
BufferedReader br = Files.newBufferedReader(path);
  • type ——— getType 和 newType 的簡版,例如:
List<Complaint> litany = Collections.list(legacyLitancy);

簡而言之,靜態工廠方法和公有構造器都各有用處,我們需要理解它們各自的長處。靜態工廠經常更加合適,因此切忌第一反應就是提供公有的構造器,而不先考慮靜態工廠。

公眾號提供 優質Java資料 以及CSDN免費下載 許可權,歡迎你關注我
Effective Java - 靜態方法與構造器

相關文章