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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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)工廠方法, 通過(guò)方法的名稱(chēng)就可以非常明確的知道我們所構(gòu)建的對(duì)象的含義, 真正意義上的提供了快捷方法.

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

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

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

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

Boolean 類(lèi)

Boolean 類(lèi)是一個(gè)比較簡(jiǎn)單的類(lèi), Boolean 的可行值實(shí)際上只有兩個(gè), True 和 False. 因此, 理論上, 在運(yùn)行過(guò)程中, Boolean 類(lèi)最多只需要?jiǎng)?chuàng)建兩個(gè)對(duì)象即可. 在 Boolean 類(lèi)內(nèi)部也是這樣實(shí)現(xiàn)的, Boolean 內(nèi)部包含兩個(gè)靜態(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 對(duì)象時(shí), 應(yīng)該始終使用的是這兩個(gè)對(duì)象, 避免創(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 時(shí), 我們應(yīng)該使用其靜態(tài)工廠方法 valueOf() 來(lái)創(chuàng)建 Boolean 對(duì)象, 或者直接使用靜態(tài)成員 TRUEFALSE.

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

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

Integer 類(lèi)

Integer 相對(duì)較復(fù)雜一些, 但是該類(lèi)在設(shè)計(jì)時(shí), 同樣擁有靜態(tài)工廠方法, 來(lá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è)構(gòu)造方法的靜態(tài)工廠方法是 valueOf(String s, int radix), valueOf(String s) 以及 valueOf(int i). 前兩個(gè)構(gòu)造函數(shù)是從 String 轉(zhuǎn)換成 Integer 對(duì)象, radix 表示進(jìn)制.

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

Integer 使用了額外的緩存機(jī)制, Integer 中有一個(gè)靜態(tài)成員類(lèi) IntegerCache, 這是一個(gè)單例類(lèi), 使用靜態(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. 在實(shí)現(xiàn)過(guò)程中, 緩存的下界只允許默認(rèn)值, 而上界允許通過(guò)設(shè)置虛擬機(jī)參數(shù)的方式進(jìn)行修改.
    1. String integerCacheHighPropValue = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    2. 可以通過(guò)虛擬機(jī)參數(shù) -XX:AutoBoxCacheMax=<size> 來(lái)設(shè)置緩存的上界. 在 JVM 初始化時(shí), 會(huì)將該值緩存到 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”? 這個(gè)問(wèn)題. 他表示這是個(gè)問(wèn)題, 但是沒(méi)有需求去修改下界.
    4. RFP 官方的說(shuō)法.
    5. 這個(gè)參數(shù)的調(diào)整只在 Integer 中存在, 在 Long 中并沒(méi)有任何可調(diào)整的緩存參數(shù), 都是設(shè)置的固定值.
  3. 在實(shí)現(xiàn)過(guò)程中, 還有一點(diǎn)需要注意的是, 緩存使用了 CDS 機(jī)制 (Class Data Sharing).

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

@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));
}

對(duì)于上述代碼的如下輸出結(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 的實(shí)現(xiàn)機(jī)制和 Integer 幾乎一致, 提供了一致的靜態(tài)工廠方法, 同時(shí)也使用了緩存機(jī)制, 這里就不再贅述了.

Double, Float

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

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

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

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

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

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

noneOf() 方法傳入的參數(shù)是一個(gè)枚舉類(lèi)的類(lèi)型信息, 其源碼實(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ù)枚舉元素的個(gè)數(shù), 確定具體的 EnumSet 實(shí)現(xiàn)方式
    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else
        return new JumboEnumSet<>(elementType, universe);
}

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

RegularEnumSet 內(nèi)部使用單個(gè) long 類(lèi)型進(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)元素個(gè)數(shù)小于等于 64 個(gè)時(shí), 使用 RegularEnumSet 就足夠了, 因?yàn)橐粋€(gè) long 類(lèi)型的數(shù)據(jù)時(shí) 64 位.

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

/**
 * 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)工廠方法返回的對(duì)象, 在編寫(xiě)靜態(tài)方法時(shí), 其對(duì)應(yīng)的類(lèi)可以不存在.

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌翘单,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掠哥,死亡現(xiàn)場(chǎng)離奇詭異梁棠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)肥矢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)端衰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人甘改,你說(shuō)我怎么就攤上這事旅东。” “怎么了十艾?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵抵代,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我忘嫉,道長(zhǎng)荤牍,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任庆冕,我火速辦了婚禮康吵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘访递。我一直安慰自己晦嵌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布拷姿。 她就那樣靜靜地躺著耍铜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跌前。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天陡舅,我揣著相機(jī)與錄音抵乓,去河邊找鬼。 笑死靶衍,一個(gè)胖子當(dāng)著我的面吹牛灾炭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颅眶,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蜈出,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了涛酗?” 一聲冷哼從身側(cè)響起铡原,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤偷厦,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后燕刻,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體只泼,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年卵洗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了请唱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡过蹂,死狀恐怖十绑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酷勺,我是刑警寧澤本橙,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站鸥印,受9級(jí)特大地震影響勋功,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜库说,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一狂鞋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧潜的,春花似錦骚揍、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至亡呵,卻和暖如春抽活,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锰什。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工下硕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人汁胆。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓梭姓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親嫩码。 傳聞我的和親對(duì)象是個(gè)殘疾皇子誉尖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355