Java最佳實踐經驗第1條:用靜態工廠方法代替構造器

haohaoxuexiyai發表於2020-12-27

首先舉個例子:

class LazyMan{
    //1、   建立一個私有的靜態的指向自己的變數
    private static LazyMan instance;

    //2、   私有化構造器,如果是第一次呼叫將建立物件例項,以後就不會再次建立
    private LazyMan() {
        System.out.println("-------建立了一個懶漢式LazyMan物件------");
    }

    //3、   建立一個公共的靜態方法,
    public static LazyMan getInstance() {
        if(instance == null) {
            instance = new LazyMan();
        }
        return instance;
    }
}

這是個懶漢式的單例設計,他沒有共有的構造器,對外只開放了一個getInstance的方法,這個方法的返回值型別是其本身,在其他類中需要使用LazyMan的物件時,只需要通過呼叫LazyMan.getInstance(),這就是一個典型的用靜態工廠方法代替構造器的案例實踐。那麼這種方式有一些什麼特點呢?唯物主義價值觀告訴我們,要辯證地看待一件事情,敲程式碼也是一樣的,正如沒有完美的程式語言一般,再優秀的設計思想也有其糟粕之處。

優勢

1、靜態工廠方法有名稱

儲備知識:獲取方法簽名javap -s 包名.類名,方法簽名中帶有引數型別和方法名
在這裡插入圖片描述
當一個類需要多個帶有相同簽名的構造器時,就可以用靜態工廠方法代替構造器,並且仔細地選擇名稱以突出靜態工廠方法之間的區別。

2、不必每次呼叫它們的時候都建立一個新物件

這個也很好舉例子,典型的應用就是在單例設計中,直接建好了一個物件作為類中的成員屬性放著,等呼叫getInstance()方法的時候把已經建立的物件傳出去,這樣永遠都只有一個物件在使用。熟悉Spring框架的小夥伴應該記得Spring的IOC容器管理也是預設用的單例模式,同時只存有一個物件,不必每次呼叫它們的時候都建立一個新物件。

這種方式還有個好處:可以使得不可變類(在後面的部落格裡詳細講)可以使用預先構建好的物件,還記得Java5版本中新增的包裝類麼?在Boolean包裝類中就有這麼一條:

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

就用到了這種思想:它沒有建立物件,而是用了預先構建好的物件,我們來看下原始碼:

在這裡插入圖片描述
其原理很類似於享元模式(挖個坑,後面補)

3、可以返回原返回型別的任何子型別

以這種形式隱藏實現類會使得API更加簡潔,很適用於基於介面的框架。這個很好理解:能使用父類的地方都可以使用其子類替代——里氏替換原則(挖個坑。後面補五大設計原則和一大設計法則)。no speaking,just coding,讓我們來寫一個demo試一下:

隨便寫個介面

public interface Fruit {
    
}

來個類實現介面,用介面的型別來作為靜態工廠方法的返回值型別,這裡要把構造器私有化防止外部呼叫

public class Apple implements Fruit{
    private String color;
    private int size;
    private Apple(String color, int size) {
        this.color = color;
        this.size = size;
    }

    @Override
    public String toString() {
        return "Apple{" +
                "color='" + color + '\'' +
                ", size=" + size +
                '}';
    }

    public static Fruit getInstance(String color, int size){
        return new Apple(color,size);
    }
}

在demo裡呼叫getInstance方法,看看能否用子型別去接收

public class Demo1 {
    public static void main(String[] args) {
        Apple apple = (Apple) Apple.getInstance("紅色", 100);
        System.out.println(apple);
    }
}

在這裡插入圖片描述

4、所返回的物件的類可以隨著每次呼叫而發生變化,這取決於靜態工廠方法的引數值

這句話有2層含義:第一,返回物件的類可以是已宣告的返回型別的所有子型別,這都是允許的。第二,方法的過載於返回值型別無關,我們可以定義多個不同引數的靜態工廠方法,對應返回不同的物件。

5、方法返回的物件所屬的類,在編寫包含該靜態工廠方法時候的類時可以不存在

聽起來比較繞,其實很好理解,這一條對應了前2條,舉個例子就明白了,比如我現在寫了一個介面Fruit,在介面裡宣告瞭一個靜態工廠方法:

public interface Fruit {
    static Fruit getInstance(String color, int size) {
        return null;
    }
}

這裡有個點要提醒下:在java8之前介面中不能存在static關鍵字,通常採用將靜態方法放在一個不可例項化的伴生類中的形式加以儲存。最典型的例子就是Java Collections Framework,所有對集合介面的工具實現都是通過靜態工廠方法在一個不可例項化的類java.util.Collections中匯出。

讓我們回來繼續之前的話題,在寫Fruit介面的時候,我還沒有想好要用什麼類來實現它,現在我想好了,我們讓一個Apple類來實現Fruit介面:

public class Apple implements Fruit{
    private String color;
    private int size;
    private Apple(String color, int size) {
        this.color = color;
        this.size = size;
    }

    @Override
    public String toString() {
        return "Apple{" +
                "color='" + color + '\'' +
                ", size=" + size +
                '}';
    }

    public static Apple getInstance(String color, int size){
        return new Apple(color,size);
    }
}

檢驗結果,同樣可以正常返回一個Apple類的物件:

public class Demo1 {
    public static void main(String[] args) {
        Apple apple = Apple.getInstance("紅色", 100);
        System.out.println(apple);
    }
}
//輸出
Apple{color='紅色', size=100}

這大大增加了靈活性,並且符合開閉原則

說完了好處,來講講缺點

1、靜態工廠方法最主要的缺點在於,類如果不包含公有的或被保護的構造器,就不能被子類例項化

例如要想將Collection Framework中的任何便利的實現類子類化,這是不可能的。但是這樣也許會因禍得福,因為它鼓勵了程式設計師使用複合(組合)(composition),而不是繼承,這正是不可變型別所需要的,也防止了繼承的濫用。

2、第二個缺點在於,程式設計師很難發現這些靜態方法

在API文件中,它們沒有像構造器那樣在API文件中明確標識出來,因此對於提供了靜態工廠方法而不是構造器的類來說,要想查明如何例項化一個類是非常困難的。Javadoc工具總有一天會注意到靜態工廠方法。同時我們現在可以通過遵守標準的命名習慣,來彌補這一劣勢。下面提供了一些慣用名稱。

靜態工廠方法的慣用名稱

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

總結

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

相關文章