Effective Java - 靜態方法與構造器

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

傳統來說,爲了使客戶端可以獲取它自身的一個實例,最傳統的方法就是提供一個公有的構造器。像下面這樣java

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.javaapp

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免費下載 權限,歡迎你關注我

相關文章
相關標籤/搜索