Effective Java: 考慮用靜態(tài)工廠方法替代構(gòu)造器

考慮用靜態(tài)工廠方法替代構(gòu)造器

這里更準(zhǔn)確的說, 是替代 public 的構(gòu)造器. 這里的靜態(tài)工廠方法指的是類中的一個靜態(tài)方法, 返回該類的一個實例 (instance). 例如 Java 的 Boolean 包裝類就提供了如下的靜態(tài)工廠方法:

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

書中為我們概括了, 使用靜態(tài)工廠方法有如下優(yōu)點:

  1. 靜態(tài)工廠方法與構(gòu)造器 (constructors) 相比, 是可以具有名稱的.
  2. 與構(gòu)造起相比, 靜態(tài)工廠方法并不要求每次調(diào)用的時候都重新創(chuàng)建一個新的對象, 這樣可以避免創(chuàng)建很多不必要的對象.
  3. 第三點優(yōu)勢是, 靜態(tài)工廠方法不僅可以創(chuàng)建當(dāng)前類的對象, 而且可以返回返回類型的任何子類對象.
  4. 第四點優(yōu)勢是, 靜態(tài)工廠方法返回的類型可以根據(jù)輸入?yún)?shù)的不同而不同.
  5. 第五點優(yōu)勢在于, 靜態(tài)工廠方法返回的對象, 在編寫靜態(tài)方法時, 其對應(yīng)的類可以不存在.

靜態(tài)工廠方法與構(gòu)造器 (constructors) 相比, 是可以具有名稱的

我們在創(chuàng)建類的時候, 有時候需要不止一種方式來產(chǎn)生對象, 一種方式是對構(gòu)造器的重載, 但是對構(gòu)造器方法的重載, 只能通過不同的參數(shù)來實現(xiàn). 有時候, 我們使用同樣的參數(shù), 也想要使用不同的方式來構(gòu)造對象, 這對于使用構(gòu)造器來說是很難實現(xiàn)的.

使用靜態(tài)工廠方法, 我們可以使用不同的命名的方式, 來使用不同的方式來構(gòu)建對象.

例如, Boolean 類, 創(chuàng)建 Boolean 對象的方法有如下幾種:

  • Boolean(boolean)
  • Boolean(String)
  • valueOf(boolean)
  • valueOf(String)

前兩個是 Boolean 的公有構(gòu)造器 (public constructor), 都接收一個參數(shù), 第一個接收 boolean 類型, 第二個接收 String 類型. 都可以分別將對應(yīng)的值轉(zhuǎn)換為 Boolean 對象.

但是這里使用構(gòu)造器意義不明確, 這兩個方法其實對應(yīng)著下面的兩個 valueOf() 方法. 這兩種用法其實是一致的, 但是, valueOf 的語意更加明確一些, valueOf 從語意上來說, 就是一種類型轉(zhuǎn)換, 代表著將 boolean 類型或者 String 類型轉(zhuǎn)換成 Boolean 的對象.

下面是一個項目中實際使用, 更加具有實際意義的例子. 在構(gòu)建網(wǎng)絡(luò)接口, 確定網(wǎng)絡(luò)接口的返回值的時候, 我們通常需要進(jìn)行一定的封裝. 例如, 如果將所有的返回類型都封裝在一個叫做 ResponseModel<M> 的泛型類中, 加入包含以下基本信息.

public class ResponseModel<M> {
    // 返回代碼
    private int code;
    // 描述信息
    private String message;
    // 創(chuàng)建時間
    private LocalDateTime time = LocalDateTime.now();
    // 具體的內(nèi)容
    private M result;
}

如果我們寫一個服務(wù)程序, 那么這個類將是我們與客戶端進(jìn)行溝通的一個非常常用的類, 我們經(jīng)常需要創(chuàng)建不同的 ResponseModel 來返回給客戶端. 因此我們最好提供不同的方法來能夠快速的使用不同的方法來創(chuàng)建不同的 ResponseModel. 例如, 請求成功的 Response (200), 包含不同類型的錯誤信息的 Response 等等. 如果使用構(gòu)造器來實現(xiàn), 是很難實現(xiàn)的, 我們需要非常謹(jǐn)慎的構(gòu)建不同的重載來實現(xiàn), 并且在調(diào)用時也是非?;靵y的, 因為在這種情況下每一個構(gòu)造方法的參數(shù)的不同并不能提供非常明確的語意以表示我們要創(chuàng)建的對象, 這可能會導(dǎo)致很大程度上的混亂, 使用上也非常不便.

如果使用靜態(tài)工廠方法, 我們就可以通過給不同的方法進(jìn)行命名, 來提供非常明確的語意信息來快速的構(gòu)建所需的對象. 例如:

public static <M> ResponseModel<M> buildOk() {
    return new ResponseModel<M>();
}

public static <M> ResponseModel<M> buildOk(M result) {
    return new ResponseModel<M>(result);
}

public static <M> ResponseModel<M> buildParameterError() {
    return new ResponseModel<M>(ERROR_PARAMETERS, "Parameters Error.");
}

上面就分別列舉了幾種不同的靜態(tài)工廠方法, 通過方法的名稱就可以非常明確的知道我們所構(gòu)建的對象的含義, 真正意義上的提供了快捷方法.

靜態(tài)工廠方法并不要求每次調(diào)用的時候都重新創(chuàng)建一個新的對象, 這樣可以避免創(chuàng)建很多不必要的對象.

這種機(jī)制對于很多值類來說, 是很常用的, 一個非常典型的例子就是 Java 中的那些裝箱類.

在 Java 中共有 8 種 primitive 類型, 這八種基本數(shù)據(jù)類型對應(yīng) 8 種自動裝箱類:

  • char -> Character
  • boolean -> Boolean
  • byte -> Byte
  • short -> Short
  • int -> Integer
  • long -> Long
  • float -> Float
  • double -> Double

Boolean 類

Boolean 類是一個比較簡單的類, Boolean 的可行值實際上只有兩個, True 和 False. 因此, 理論上, 在運行過程中, Boolean 類最多只需要創(chuàng)建兩個對象即可. 在 Boolean 類內(nèi)部也是這樣實現(xiàn)的, Boolean 內(nèi)部包含兩個靜態(tài)成員:

/**
 * The {@code Boolean} object corresponding to the primitive
 * value {@code true}.
 */
public static final Boolean TRUE = new Boolean(true);

/**
 * The {@code Boolean} object corresponding to the primitive
 * value {@code false}.
 */
public static final Boolean FALSE = new Boolean(false);

在我們使用 Boolean 對象時, 應(yīng)該始終使用的是這兩個對象, 避免創(chuàng)建額外的變量, 這樣能夠方便我們使用.

Boolean 的 public 構(gòu)造方法在新版本的 JDK 中已經(jīng)被標(biāo)記為 @Deprecated.

@Deprecated(since="9")
public Boolean(boolean value) {
    this.value = value;
}

@Deprecated(since="9")
public Boolean(String s) {
    this(parseBoolean(s));
}

在使用 Boolean 時, 我們應(yīng)該使用其靜態(tài)工廠方法 valueOf() 來創(chuàng)建 Boolean 對象, 或者直接使用靜態(tài)成員 TRUEFALSE.

關(guān)于自動裝箱和自動拆箱, 我查到有資料說是會自動調(diào)用對應(yīng)的 valueOf() 方法.

只要不在外部調(diào)用 Boolean 的構(gòu)造方法 (我們也不應(yīng)該調(diào)用), 程序在運行過程中就只存在兩個靜態(tài)對象.

Integer 類

Integer 相對較復(fù)雜一些, 但是該類在設(shè)計時, 同樣擁有靜態(tài)工廠方法, 來代替構(gòu)造器. Integer 的構(gòu)造器同樣也被標(biāo)記為 @Deprecated, 我們同樣不應(yīng)該使用.

@Deprecated(since="9")
public Integer(int value) {
    this.value = value;
}

@Deprecated(since="9")
public Integer(String s) throws NumberFormatException {
    this.value = parseInt(s, 10);
}

同樣的, 用于替代上面兩個構(gòu)造方法的靜態(tài)工廠方法是 valueOf(String s, int radix), valueOf(String s) 以及 valueOf(int i). 前兩個構(gòu)造函數(shù)是從 String 轉(zhuǎn)換成 Integer 對象, radix 表示進(jìn)制.

Integer 與 Boolean 相比的額外的機(jī)制是緩存機(jī)制, Boolean 對象只有兩個取值, 因此直接使用兩個靜態(tài)成員變量即可.

Integer 使用了額外的緩存機(jī)制, Integer 中有一個靜態(tài)成員類 IntegerCache, 這是一個單例類, 使用靜態(tài)代碼塊進(jìn)行了初始化.

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer[] cache;
    static Integer[] archivedCache;

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                h = Math.max(parseInt(integerCacheHighPropValue), 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        // Load IntegerCache.archivedCache from archive, if possible
        VM.initializeFromArchive(IntegerCache.class);
        int size = (high - low) + 1;

        // Use the archived cache if it exists and is large enough
        if (archivedCache == null || size > archivedCache.length) {
            Integer[] c = new Integer[size];
            int j = low;
            for(int i = 0; i < c.length; i++) {
                c[i] = new Integer(j++);
            }
            archivedCache = c;
        }
        cache = archivedCache;
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}
  1. IntegerCache 的緩存范圍默認(rèn)是 -128 ~ 127.
  2. 在實現(xiàn)過程中, 緩存的下界只允許默認(rèn)值, 而上界允許通過設(shè)置虛擬機(jī)參數(shù)的方式進(jìn)行修改.
    1. String integerCacheHighPropValue = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    2. 可以通過虛擬機(jī)參數(shù) -XX:AutoBoxCacheMax=<size> 來設(shè)置緩存的上界. 在 JVM 初始化時, 會將該值緩存到 jdk.internal.misc.VM 中的 java.lang.Integer.IntegerCache.high 屬性中.
    3. 關(guān)于為什么只允許修改上界而不允許修改下界, 我查到 Why does the JVM allow to set the “high” value for the IntegerCache, but not the “l(fā)ow”? 這個問題. 他表示這是個問題, 但是沒有需求去修改下界.
    4. RFP 官方的說法.
    5. 這個參數(shù)的調(diào)整只在 Integer 中存在, 在 Long 中并沒有任何可調(diào)整的緩存參數(shù), 都是設(shè)置的固定值.
  3. 在實現(xiàn)過程中, 還有一點需要注意的是, 緩存使用了 CDS 機(jī)制 (Class Data Sharing).

由于有上述緩存機(jī)制, 我們進(jìn)行如下測試:

@SuppressWarnings({"NumberEquality"})
public static void testInteger() {
    System.out.println("\n=====Some test for Integer=====\n");
    Integer a = 1000, b = 1000;
    System.out.println("a = " + a + ", b = " + b);
    // Warning: Only for test, don't use "==" to compare two boxed object
    System.out.println("a == b: " + (a == b));

    Integer c = 100, d = 100;
    System.out.println("c = " + c + ", d = " + d);
    // Warning: Only for test, don't use "==" to compare two boxed object
    System.out.println("a == b: " + (c == d));
}

對于上述代碼的如下輸出結(jié)果就比較容易理解了:

=====Some test for Integer=====

a = 1000, b = 1000
a == b: false
c = 100, d = 100
a == b: true

Char, Byte, Long 和 Short

Char, Byte, Long 和 Short 的實現(xiàn)機(jī)制和 Integer 幾乎一致, 提供了一致的靜態(tài)工廠方法, 同時也使用了緩存機(jī)制, 這里就不再贅述了.

Double, Float

Double 和 Float 也提供了靜態(tài)工廠方法 valueOf(), 但是并沒有提供緩存機(jī)制, 因為小數(shù)并不適合進(jìn)行緩存.

靜態(tài)工廠方法不僅可以創(chuàng)建當(dāng)前類的對象, 而且可以返回返回類型的任何子類對象.

這一特性的一個應(yīng)用是在不暴露子類的具體實現(xiàn)的情況下, 返回一個子類對象. 例如 Java Collections Framework. 在一個叫做 java.util.Collections 的伴生類中, 實現(xiàn)了不可修改集合 (UnmodifiableSet), 同步集合 (SynchronizedSet), 空集合 (EmptySet) 等等這些工具集合.

這些集合的實現(xiàn), 都是非公有的, 如果想要獲取這些類的對象, 就可以調(diào)用 Collections 中對應(yīng)的靜態(tài)工廠方法, 并使用接口去引用這些對象.

靜態(tài)工廠方法返回的類型可以根據(jù)輸入?yún)?shù)的不同而不同.

EnumSet 是一個抽象類, 其沒有提供公有構(gòu)造方法, 其提供了一系列的靜態(tài)工廠方法來創(chuàng)建 EnumSet, 包括 noneOf(), allOf(), of(), range(). 這一系列靜態(tài)工廠方法最終調(diào)用的都是 noneOf() 方法.

noneOf() 方法傳入的參數(shù)是一個枚舉類的類型信息, 其源碼實現(xiàn)如下.

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    // 獲取所有枚舉的數(shù)組
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
        throw new ClassCastException(elementType + " not an enum");

    // 根據(jù)枚舉元素的個數(shù), 確定具體的 EnumSet 實現(xiàn)方式
    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else
        return new JumboEnumSet<>(elementType, universe);
}

可以看出, 其最終返回的是一個 RegularEnumSet 對象或者 JumboEnumSet 對象. 這兩個類都是 EnumSet 的具體實現(xiàn). 是根據(jù)枚舉元素的具體個數(shù), 從而確定 EnumSet 的具體實現(xiàn).

RegularEnumSet 內(nèi)部使用單個 long 類型進(jìn)行支持:

/**
 * Bit vector representation of this set.  The 2^k bit indicates the
 * presence of universe[k] in this set.
 */
private long elements = 0L;

當(dāng)元素個數(shù)小于等于 64 個時, 使用 RegularEnumSet 就足夠了, 因為一個 long 類型的數(shù)據(jù)時 64 位.

當(dāng)元素個數(shù)大于 64 時, 使用 JumboEnumSet 實現(xiàn), 其內(nèi)部使用一個 long 數(shù)組進(jìn)行存儲.

/**
 * Bit vector representation of this set.  The ith bit of the jth
 * element of this array represents the  presence of universe[64*j +i]
 * in this set.
 */
private long elements[];

靜態(tài)工廠方法返回的對象, 在編寫靜態(tài)方法時, 其對應(yīng)的類可以不存在.

其典型應(yīng)用時 JDBC 的應(yīng)用模式.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容