Effective Java

Effective Java - 靜態方法與構造器

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

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

<code>public class Apple {    public Apple(){}    public static void main(String[] args) {        Apple apple = new Apple();    }}/<code>

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

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

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

<code>public static final Boolean TRUE = new Boolean(true);/<code>

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

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

<code>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);    }}/<code>

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

靜態工廠有名稱

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

BigInteger.java

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

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

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

還是BigInteger.java

<code>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) {...}/<code>

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

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

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

靜態工廠不必重新創建一個對象

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

享元模式

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

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

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

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

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

實例受控的類

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

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

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

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

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

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

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

<code>public class UtilityClass {  private UtilityClass(){    throw new AssertionError();  }}/<code>

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

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

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

靜態工廠可以返回任何子類型對象

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

Collections.java

<code>public static  Collection unmodifiableCollection(Collection extends T> c) {  return new UnmodifiableCollection<>(c);}static class UnmodifiableCollection implements Collection, Serializable {  ... UnmodifiableCollection(Collection extends E> c) {    if (c==null)      throw new NullPointerException();    this.c = c;  }   ...}/<code>

這是Collections.java 中的代碼片段,靜態方法unmodifiableCollection返回一個新的UnmodifiableCollection,調用它的靜態方法創建UnmodifiableCollection的對象,由於UnmodifiableCollection繼承於Collection,也就是說靜態方法unmodifiableCollection其實是返回了一個子類的對象。

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

靜態工廠的第四大優勢在於,所返回的對象的類可以隨著每次調用而發生變化,這取決於靜態工廠方法的參數值。只要是已聲明的返回類型的子類型,都是允許的。返回對象的類也可能隨著發行版本的不同而不同。

EnumSet 沒有公有的構造器,只有靜態工廠方法。在OpenJdk實現中,它們返回兩種子類之一的一個實例,具體則取決於底層枚舉類型的大小:如果它的元素有64個或者更少,就像大多數枚舉類型一樣,靜態工廠方法就會返回一個RegularEnumSet實例,用單個long進行支持;如果枚舉類型有65個或者更多元素,工廠就返回JumboEnumSet實例,用一個long數組進行支持。

<code>public static > EnumSet noneOf(Class 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);}/<code>

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

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

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

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

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

<code>123456789101112131415161718192021222324252627282930313233343536/<code>
<code>//四大組成之一:服務接口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> providers = new HashMap<string>();//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();    }}/<string>/<string>/<code> 

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

靜態工廠方法的缺點

靜態工廠方法依賴於構造函數的創建

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

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

靜態工廠方法很難被發現

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

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

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


分享到:


相關文章: