建立和銷燬物件

Xbigfat發表於2019-01-15

第 1 條:考慮用靜態工廠方法代替構造器

對於類而言,為了讓客戶端獲取它自身的一個例項,常用的方法是提供一個公共的構造器。 還有一種方法:類可以提供一個共有的靜態工廠方法,它只是一個返回類的例項的靜態方法。。下面是一個返回 Boolean 類的例項的例子:

public static Boolean valueOf(boolean b){
    return b ? Boolean.TRUE : Boolean.FALSE;
}
複製程式碼

靜態工廠方法,不等同於設計模式中的工廠方法 這樣做的幾大優勢:

  • 靜態工廠方法與構造方法不同的第一個優勢在於,方法具有名稱。構造方法本身不能帶有任何具有字面意義的方法簽名,無法從方法名字中推測出方法呼叫後,產生的例項的作用或者配置;而靜態工廠方法可以增加方法簽名,例如 BigInteger(int,int,Random) 返回的 BigInteger 可能為素數,如果用名為 BigInteger.proablePrime 的靜態工廠方法,顯然更為清楚。
  • 不必在每次呼叫它們的時候,都建立一個新的物件。這使得不可變類可以使用預先構建好的例項,或者將構建好的例項快取起來,重複利用,避免重複建立物件。 Boolean.valueOF(boolean) 這個方法就說明了這一點:它從來不需要建立物件,直接返回一個靜態成員變數。
  • 第三個優勢:它們可以返回 原型別 的任何子型別的物件。這樣的靈活性在於:API 可以返回申明型別的子型別,同時又能確保返回的子類不會變成公開的。Java Collections Framework 的集合介面共有32個便利實現,分別提供了不可修改的集合、同步集合等。幾乎所有這些實現都通過靜態工廠方法在一個不可例項化的類中匯出,返回物件的類都是非公有的。
  • 公有的靜態工廠方法,所返回的物件的類不僅可以是私有的,而且該類還可以隨著每次呼叫發生變化。例如:
public static Number getInstance(int b){
    if(b > 0){
    return new NumberPositive();
    else{
    return new NumberNegative();
    }
}
複製程式碼

java.util.EmunSet 沒有公共構造方法,只有靜態工廠方法,它們返回兩種實現之一,取決於底層列舉型別的大小。小於等於 64 個時,會返回 RegalarEnumSet 例項,用 long 進行支援;大於 64 個則返回 JumboEnumSet 例項,使用 long[] 進行支援。

靜態工廠返回的類,在編寫時該類可以不需要存在,這種靈活的方法構成了服務提供者框架基礎。例如 JDBC API。它們提供一個服務,系統為客戶端提供多個實現,並把它們從多個實現中解耦出來。

服務提供者框架中有三個重要元件:

  • 服務介面 Service Interface :提供者實現
  • 提供者註冊 API :系統用來註冊實現,讓客戶端訪問它
  • 服務訪問 API :客戶端用來獲取服務的例項的。

這一段其實我也有點沒看懂。

  • 第四個優點:建立泛型例項的時候,它們使程式碼變得更加簡潔。例子:
Map<String,List<String>> m = new HashMap<String,List<String>>();
複製程式碼

如果出現了很長的泛型,那麼泛型列表也將變得冗餘。有了靜態工廠方法,編譯器可以找到引數型別,這稱作型別推導

public static <K,V> HashMap<K,V> newInstance(){
    return new HashMap<K,V>();
}

Map<String,List<String>> m = HashMap.newInstance();
複製程式碼

但是它的缺點在於:類如果不含有公共的或者受保護的構造方法,就不能被子類化,對於公有的靜態方法所返回的非共有類,同樣如此。例如,想要將 Collections Framework 中的任何方便的實現類子類化,是無法實現的。 第二個缺點在於,它們與其他的靜態方法沒有任何區別。在 API 文件中,並不能明顯地突出它們。我們在使用是,應當遵守一些名稱規範,便於使用:

  • valueOf - 該方法返回的例項與它的引數具有相同的值,實際上是一個型別轉換方法
  • of - 簡單的替代 valueOf
  • getInstance - 獲取一個例項,可能內部使用單例實現
  • newInstance - 明確宣告,獲取一個新的例項

第 2 條:遇到多個構造器引數時,要考慮 Builder 模式

靜態工廠方法和構造方法都不能很好的處理大量的可選引數的初始化。例如建立一個食物物件 Food,它包含重量、價格、名稱意外,可能還有很多食物屬性。 對於這樣的類,為每個可選屬性都編寫一個構造方法,很顯然是不明智的操作。這裡不上程式碼了。 利用構造方法的缺點很明顯:

  • 不能保證傳入構造器的引數一定合理(順序傳錯了不會得到錯誤,但是可能出現業務邏輯錯誤)
  • 修改、擴充性繁瑣

另外還有一種方法,那就是通過 Java Bean 的方法。在建立物件後,呼叫所有需要初始化的屬性的 setter 方法,注入屬性值。這也很麻煩。

Person person = new Person();
person.setName("Xbigfat");
person.isMale(true);
person.setAge(24);
...
複製程式碼

而且可能遺漏某個屬性,造成業務邏輯出錯。此外,這些成員變數還不可以使用 final 修飾,程式設計師需要付出額外的精力來確保執行緒安全。 好了,引出 Builder 模式吧。建造者模式不直接生產物件,而是通過 Bean 中的靜態內部類鏈式注入需要的所有屬性,最後生成物件。 在呼叫過程中,可以及時判斷出不符合邏輯的屬性,並跑出檢查異常,在建造過程中,可以找到遺忘的屬性,丟擲執行時異常。

public class Person{
//實體屬性
    private final String name;
    private final boolean isMale;
    private final int age;
    private final int weight;
    
    public static class Builder{
        //必須引數
        private final String name;
        private final boolean isMale;
        
        //可選引數
        private final int age;
        private final int weight;
        
        //構建 Builder 物件
        public Builder(String name,boolean isMale){
            this.name = name;
            this.isMale = isMale;
        }
        
        //鏈式呼叫傳入屬性
        public Builder(int age){
            this.age = age;
            return this;
        }
        
        //鏈式呼叫傳入屬性
        public Builder(int weight){
            this.weight = weight
            return this;
        }
        
        //建立物件並返回
        public Person build(){
            return new Person(this);
        }
    }
    
    //讀取建造者傳遞的屬性,生成物件
    private Person(Builser builder){
        this.name = builder.name;
        this.isMale = builder.isMale;
        this.age = builder.age;
        this.weight = builder.weight;
    }
}

//客戶端程式碼:
Person person = new Person.Builder("Xbigfat",true).age(24).build();
複製程式碼

簡潔明朗,美美噠。 Builder 的優勢就在於,每個屬性的操作都是獨立的,不必重新編寫優化構造方法。

第 3 條:用私有構造方法或者列舉型別強化 Singleton 屬性

Signleton 指僅僅被例項化一次的類,在程式執行期間,只會生成一個物件,例項化一次。下面是一種實現 Singleton 的方法:

public class Singleton{
    private static final Singleton instance = new Singleton();
    private Singleton(){
    //將構造方法私有化
    }
    
    public stacic Singleton getInstance(){
        return instance;
    }
}
複製程式碼

私有構造器僅能夠被 Singleton 的成員變數呼叫一次,一旦成員變數 instance 被初始化後(靜態的會自動初始化),final 修飾將確保其引用不可改變。但這種方式並不能防止反射的方式,構建新的物件。 為了使這種方法實現的單例物件能夠序列化,僅僅在宣告中加上 implements Serializable 是不妥的,還需要將所有的例項宣告為 transient 並提供一個 readResolve 方法。否則,每次反序列化時都會生成一個新的例項。 從 JDK 1.5 開始,實現 Singleton 還有一種方法:編寫一個包含單個元素的列舉型別:

public enum Singleton{
    instance;
    ...
}
複製程式碼

這種方法簡介,並提供了序列化機制,絕對防止多次例項化,單元素的列舉型別已經成為實現 Singleton 的最佳方法。

第 4 條:通過私有化構造方法,強化不可例項化的屬性

有時候可能需要編寫 只包含 靜態方法 和 靜態域 的類。有些人可能在程式設計中濫用這樣的類來編寫過程化的程式。我們可以利用這種類,以 java.lang.Math / java.util.Arrays 的方式,把基本型別的值或者陣列型別上的相關方法組織起來。我們也可以通過 java.util.Collections 的方式,把實現特定介面的物件上的靜態方法組織起來。 這樣的工具類不需要被例項化,例項化它們沒有意義。然而,在沒有宣告構造方法的情況下,編譯器會自動為它們生成一個公共的、無參的預設構造方法。 企圖通過將類做成 抽象類 來強制該類不可被例項化也是行不通的,該類可以被繼承,並且子類可以例項化,這樣做甚至會誤導使用者需要自己實現子類。 最佳實踐方式:

public class UtilClass{
    private UtilClass(){
        throw new UnsupportedOperationError();
    }
}
複製程式碼

第 5 條:避免建立不必要的物件

一般來說,最好是能夠重用物件,而不是每次都新建一個物件。重用的方式快速、節省資源。如果物件是不可變的,它就始終可以被重用。 反面教材:

String s = new Sting("stringette");
複製程式碼

該語句每次被執行的時候,都會建立一個新的 String 例項,但是這些建立物件的動作全都是不必要的。傳遞給 String 的構造引數本身就是一個 String 例項,與構造方法建立的物件完全一樣。如果這種用法出現在迴圈中,就會出現許多個完全不必要的例項。改進後的版本:

String s = "stringette";
複製程式碼

這種方式只用了一個 String 的例項,而不是每次執行的時候都建立新的例項。而且,它可以保證,對於在同一臺虛擬機器中執行的程式碼,只要它們包含相同的字串字面常量,該物件就會被重用。 對於同時提供了靜態工廠方法和構造器的不可變類,通常可以使用靜態工廠方法而不是構造方法,以避免重複建立物件。例如 Boolean.valueOf() 方法,不會每次都重複建立物件。 兩個例子,避免建立重複物件後,效率增加:

  • 計算一個人的出生日期是否在某個區間內。由於 Calendar 這個物件是固定的,所以建立一次即可,在比較的時候,可以重用該物件。頻繁的建立該物件,將浪費大量資源
  • 避免自動裝箱帶來的效能損耗,優先使用基本型別,當心不必要的自動裝箱
public stativ void main(String[] args){
    Long sum = 0L;
    for(long i = 0; i < Ingeger,MAX_VALUE ; i++){
        sum += i;
    }
}
複製程式碼

這段程式的計算結果是正確的,但是 Long 不等於 long,自動裝箱將 long 轉為 Long 時,大約建立了 MAX_VALUE 個 Long 物件,帶來的差異可想而知。在我自己的測試中,分別用了 700ms 和 7000ms。

當然,也不是一味的需要避免建立物件,在某些小物件的建立下,現有 JVM 不會造成太大的代價,能夠提升程式閱讀流暢、簡潔性的建立物件,是可取的做法。

第 6 條:消除過期物件的引用

及時將不需要的變數設定為 null 可以避免記憶體洩漏。

相關文章