【Effective Java】建立和銷燬對象

1、考慮用靜態工廠方法代替構造器

構造器是建立一個對象實例的最基本最經常使用的方法。開發者在使用某個類的時候,一般會使用new一個構造器來實現,其實也有其餘方式能夠實現的,如利用發射機制。這裏主要說的是經過靜態類工廠的方式來建立class的實例,如:html

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

靜態工廠方法和構造器不一樣有如下主要優點:java

1. 有意義的名稱

可能有多個構造器,不一樣構造器有不一樣的參數,而參數自己並不能確切地描述被返回的對象,因此顯得有點模糊,而具備適當名稱的靜態工廠可讀性更強,表達也更清晰。數組

如,構造器BigInteger(int, int, Random)返回一個BigInteger多是一個素數,更名爲BigInteger.probablePrime的靜態工廠方法表示也就更加清晰。緩存

2. 沒必要在每次調用的時候建立一個新的對象

這樣能夠避免建立沒必要要的重複對象,提升程序效率。安全

3. 能夠返回原返回類型的任何子類型的對象

Java的不少服務提供者框架(ServiceProvider Framework,三個主要組件:服務接口(Service Interface)這是提供者實現的;提供者註冊API(Provider Registration API),這是系統用來註冊實現,讓客戶端訪問它的;服務訪問API(Service Access API),是客戶端用來獲取服務實例的。可選組件:服務提供者接口(Service Provider Interface),提供者負責建立其服務實現的實例)都運用到這個特性,如JDBC的API。多線程

下面是一個包含一個服務提供者接口和一個默認提供者:框架

//Service interface
public interface Service{
    //...service methods
}
//Service provider interface
public interface Provider{
    Service newService();
}
//noninstantiable class for service registration and access
public class Service{
    private Service(){}

    //Maps service names to services
    private static final Map<String, Provider> providers=new ConcurrentHashMap<String, Provider>();
    public static final String DEFAULT_PROVIDER_NAME="<def>";
    //Provider registration API
    public static void registerDefaultProvider(Provider p){
        registerProvider(DEFAULT_PROVIDER_NAME);
    }
    public static void registerProvider(String name,Provider p){
        providers.put(name, p);
    }
    //Service access API
    public static Service newInstance(){
        return newInstace(DEFAULT_PROVIDER_NAME);
    }
    public static Service newInstance(String name){
        Provider p=providers.get(name);
        if(p==null)
            throw new IllegalArgumentException("No provider registered with name:"+name);
        return p.newService();
    }
}

4. 在建立參數化類型實例的時候,它們使代碼變得更加簡潔

原來:dom

Map<String, List<String>> map=new HashMap<String, List<String>>();

改成靜態工廠方法,能夠利用參數類型推演的優點,避免了類型參數在一次聲明中被屢次重寫所帶來的煩憂:ide

public static <K,V> HashMap<K,V> newInstance() {
    return new HashMap<K,V>();
}
Map<String,String> m = MyHashMap.newInstance();

固然,靜態方法也存在缺點:函數

  1. 類若是不含公有的或者受保護的構造器,就不能被子類化;
  2. 與其餘的靜態方法實際上沒有任何區別(API沒有像構造器那樣標識出來)

不過,對於靜態工廠方法和構造器,一般優先考慮靜態工廠方法。

2、遇到多個構造參數時要考慮用構建器(Builder模式)

靜態工廠和構造器有一個共同的侷限性:它們都不能很好地擴展到大量的可選參數。固然能夠經過如下方法解決:

方法一:利用重疊構造器模式(就是須要多少個參數就在參數列表添加多少個),可是當有不少個參數時,客戶端代碼會很難編寫,而且難以閱讀;

方法二:JavaBeans模式,調用一個無參構造函數,而後調用setter方法來設置每一個必要的參數,但調用的過程當中可能會出現不一致的狀態,調試比較麻煩;

方法三:Builder模式。不直接生成想要的對象,而是讓客戶端利用全部必要的參數調用構造器(或靜態方法),獲得一個builder對象,而後再在builder對象對每一個參數對應的方法進行調用來設置,以下:

class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    public static class Builder {
        //對象的必選參數
        private final int servingSize;
        private final int servings;
        //對象的可選參數的缺省值初始化
        private int calories = 0;
        private int fat = 0;
        private int carbohydrate = 0;
        private int sodium = 0;
        //只用少數的必選參數做爲構造器的函數參數
        public Builder(int servingSize,int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
        public Builder calories(int val) {
            calories = val;
            return this;
        }
        public Builder fat(int val) {
            fat = val;
            return this;
        }
        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}
//使用方式
public static void main(String[] args) {
    NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100)
        .sodium(35).carbohydrate(27).build();
    System.out.println(cocaCola);
}

因此,若是類的構造器或者靜態工廠中具備多個參數,設計這種類時,Builder模式就是種不錯的選擇!

3、用私有構造器或者枚舉類型強化Singleton屬性

Singleton模式常常會被用到,它被用來表明那些本質上惟一的系統組件,如窗口管理器或者文件系統。在Java中實現單例模式主要有三種:

  1. 將構造函數私有化,直接經過靜態公有的final域字段獲取單實例對象:

    public class Elvis {
        public static final Elvis INSTANCE = new Elvis();
        private Elivs() { ... }
        public void leaveTheBuilding() { ... }
    }

    這樣的方式主要優點在於簡潔高效,使用者很快就能斷定當前類爲單實例類,在調用時直接操做Elivs.INSTANCE便可,因爲沒有函數的調用,所以效率也很是高效。然而事物是具備必定的雙面性的,這種設計方式在一個方向上走的過於極端了,所以他的缺點也會是很是明顯的。若是從此Elvis的使用代碼被遷移到多線程的應用環境下了,系統但願可以作到每一個線程使用同一個Elvis實例,不一樣線程之間則使用不一樣的對象實例。那麼這種建立方式將沒法實現該需求,所以須要修改接口以及接口的調用者代碼,這樣就帶來了更高的修改爲本

  2. 經過公有域成員的方式返回單實例對象:

    public class Elvis {
        public static final Elvis INSTANCE = new Elvis();
        private Elivs() { ... }
        public static Elvis getInstance() { return INSTANCE; }
        public void leaveTheBuilding() { ... }
    }

    這種方法很好的彌補了第一種方式的缺陷,若是從此須要適應多線程環境的對象建立邏輯,僅須要修改Elvis的getInstance()方法內部便可,對用調用者而言則是不變的,這樣便極大的縮小了影響的範圍。至於效率問題,現今的JVM針對該種函數都作了很好的內聯優化,所以不會產生因函數頻繁調用而帶來的開銷。

  3. 使用枚舉的方式(Java SE5):

    public enum Elvis {
        INSTANCE;
        public void leaveTheBuilding() { ... }
    }

    就目前而言,這種方法在功能上和公有域方式相近,可是他更加簡潔更加清晰,擴展性更強也更加安全。雖然這種方法還沒被普遍採用,但單元素的枚舉類型已經成爲實現Singleton的最佳方法。

4、經過私有構造器強化不可實例化的能力

對於有些工具類如java.lang.Math、java.util.Arrays等,其中只是包含了靜態方法和靜態域字段,所以對這樣的class實例化就顯得沒有任何意義了。然而在實際的使用中,若是不加任何特殊的處理,這樣的classes是能夠像其餘classes同樣被實例化的。這裏介紹了一種方式,既將缺省構造函數設置爲private,這樣類的外部將沒法實例化該類,與此同時,在這個私有的構造函數的實現中直接拋出異常,從而也避免了類的內部方法調用該構造函數

public class UtilityClass {
    //Suppress default constructor for noninstantiability.
    private UtilityClass() {
        throw new AssertionError();
    }
}

這樣定義以後,該類將不會再被外部實例化了,不然會產生編譯錯誤。然而這樣的定義帶來的最直接的負面影響是該類將不能再被子類化。

5、避免建立沒必要要的對象

通常來講,最好能重用對象而不是在每次須要的時候建立一個相同功能的新對象。

試比較如下兩行代碼在被屢次反覆執行時的效率差別:

String s = new String("stringette"); //don't do this
    String s = "stringette";

因爲String被實現爲不可變對象,JVM底層將其實現爲常量池,既全部值等於"stringette" 的String對象實例共享同一對象地址,並且還能夠保證,對於全部在同一JVM中運行的代碼,只要他們包含相同的字符串字面常量,該對象就會被重用。

咱們繼續比較下面的例子,並測試他們在運行時的效率差別:

Boolean b = Boolean.valueOf("true");
    Boolean b = new Boolean("true");

前者經過靜態工廠方法保證了每次返回的對象,若是他們都是true或false,那麼他們將返回相同的對象。換句話說,valueOf將只會返回Boolean.TRUE或Boolean.FALSE兩個靜態域字段之一。然後面的Boolean構造方式,每次都會構造出一個新的Boolean實例對象。這樣在屢次調用後,第一種靜態工廠方法將會避免大量沒必要要的Boolean對象被建立,從而提升了程序的運行效率,也下降了垃圾回收的負擔。

繼續比較下面的代碼:

public class Person {
    private final Date birthDate;
    //判斷該嬰兒是不是在生育高峯期出生的。
    public boolean isBabyBoomer {
        Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        c.set(1946,Calendar.JANUARY,1,0,0,0);
        Date dstart = c.getTime();
        c.set(1965,Calendar.JANUARY,1,0,0,0);
        Date dend = c.getTime();
        return birthDate.compareTo(dstart) >= 0 && birthDate.compareTo(dend) < 0;
    }
}
//修改後
public class Person {
    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        c.set(1946,Calendar.JANUARY,1,0,0,0);
        BOOM_START = c.getTime();
        c.set(1965,Calendar.JANUARY,1,0,0,0);
        BOOM_END = c.getTime();
    }
    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
    }
}

改進後的Person類只是在初始化的時候建立Calender、TimeZone和Date實例一次,而不是在每次調用isBabyBoomer方法時都建立一次他們。若是該方法會被頻繁調用,效率的提高將會極爲顯著。

集合框架中的Map接口提供keySet方法,該方法每次都將返回底層原始Map對象鍵數據的視圖,而並不會爲該操做建立一個Set對象並填充底層Map全部鍵的對象拷貝。所以當屢次調用該方法並返回不一樣的Set對象實例時,事實上他們底層指向的將是同一段數據的引用。

在該條目中還提到了自動裝箱行爲給程序運行帶來的性能衝擊,若是能夠經過原始類型完成的操做應該儘可能避免使用裝箱類型以及他們之間的交互使用。見下例:

public static void main(String[] args) {
    Long sum = 0L;  //注意Long與long
    for (long i = 0; i < Integer.MAX_VALUE; ++i) {
        sum += i;
    }
    System.out.println(sum);
}

本例中因爲錯把long sum定義成Long sum,其效率下降了近10倍,這其中的主要緣由即是該錯誤致使了2的31次方個臨時Long對象被建立了。要優先使用基本類型而不是裝箱基本類型,要小心無心識的自動裝箱。

6、消除過時的對象引用

儘管Java的JVM垃圾回收機制對內存進行智能管理了,不像C++那樣須要手動管理,但只是由於如此,Java中內存泄露變得更加隱匿,更加難以發現,見以下代碼:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    public Object pop() {
        if (size == 0) 
            throw new EmptyStackException();
        return elements[--size];
    }
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copys(elements,2*size+1);
    }
}

以上示例代碼,在正常的使用中不會產生任何邏輯問題,然而隨着程序運行時間不斷加長,內存泄露形成的反作用將會慢慢的顯現出來,如磁盤頁交換、OutOfMemoryError等。那麼內存泄露隱藏在程序中的什麼地方呢?當咱們調用pop方法是,該方法將返回當前棧頂的elements,同時將該棧的活動區間(size)減一,然而此時被彈出的Object仍然保持至少兩處引用,一個是返回的對象,另外一個則是該返回對象在elements數組中原有棧頂位置的引用。這樣即使外部對象在使用以後再也不引用該Object,那麼它仍然不會被垃圾收集器釋放,長此以往致使了更多相似對象的內存泄露。修改方式以下:

public Object pop() {
    if (size == 0) 
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; //手工將數組中的該對象置空
    return result;
}

因爲現有的Java垃圾收集器已經足夠只能和強大,所以沒有必要對全部不在須要的對象執行obj = null的顯示置空操做,這樣反而會給程序代碼的閱讀帶來沒必要要的麻煩,該條目只是推薦在如下3中情形下須要考慮資源手工處理問題:

  1. 類是本身管理內存,如例子中的Stack類。
  2. 使用對象緩存機制時,須要考慮被從緩存中換出的對象,或是長期不會被訪問到的對象。
  3. 事件監聽器和相關回調。用戶常常會在須要時顯示的註冊,然而卻常常會忘記在不用的時候註銷這些回調接口實現類。

7、避免使用終結方法

終結方法(finalizer)一般是不可預測的,也是很危險的,通常狀況下是沒必要要的。使用終結方法會致使行爲不穩定、下降性能,以及可移植性問題。

在Java中完成這樣的工做主要是依靠try-finally機制來協助完成的。然而Java中還提供了另一種被稱爲finalizer的機制,使用者僅僅須要重載Object對象提供的finalize方法,這樣當JVM的在進行垃圾回收時,就能夠自動調用該方法。可是因爲對象什麼時候被垃圾收集的不肯定性,以及finalizer給GC帶來的性能上的影響,所以並不推薦使用者依靠該方法來達到關鍵資源釋放的目的。好比,有數千個圖形句柄都在等待被終結和回收,惋惜的是執行終結方法的線程優先級要低於普通的工做者線程,這樣就會有大量的圖形句柄資源停留在finalizer的隊列中而不能被及時的釋放,最終致使了系統運行效率的降低,甚至還會引起JVM報出OutOfMemoryError的錯誤。

Java的語言規範中並無保證該方法會被及時的執行,甚至都沒有保證必定會被執行。即使開發者在code中手工調用了 System.gcSystem.runFinalization 這兩個方法,這僅僅是提升了finalizer被執行的概率而已。還有一點須要注意的是,被重載的finalize()方法中若是拋出異常,其棧幀軌跡是不會被打印出來的。在Java中被推薦的資源釋放方法爲,提供顯式的具備良好命名的接口方法,如 FileInputStream.close()Graphic2D.dispose() 等。而後使用者在finally區塊中調用該方法,見以下代碼:

public void test() {
    FileInputStream fin = null;
    try {
        fin = new FileInputStream(filename);
        //do something.
    } finally {
        fin.close();
    }
}

在實際的開發中,利用finalizer又能給咱們帶來什麼樣的幫助呢?見下例:

public class FinalizeTest {
    //@Override
    protected void finalize() throws Throwable {
        try {
            //在調試過程當中經過該方法,打印對象在被收集前的各類狀態,
            //如判斷是否仍有資源未被釋放,或者是否有狀態不一致的現象存在。
            //推薦將該finalize方法設計成僅在debug狀態下可用,而在release
            //下該方法並不存在,以免其對運行時效率的影響。
            System.out.println("The current status: " + _myStatus);
        } finally {
            //在finally中對超類finalize方法的調用是必須的,這樣能夠保證整個class繼承
            //體系中的finalize鏈都被執行。
            super.finalize(); 
        }
    }
}

整理參考自《Effective Java》和 Effective Java (建立和銷燬對象)

相關文章
相關標籤/搜索