《Effective Java》——學習筆記(創建和銷燬對象)

創建和銷燬對象

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

靜態工廠方法與公有的構造器相比,具有以下幾大優勢:

  • 優勢一:靜態工廠方法有名稱

    具有適當名稱的靜態工廠更容易使用,產生的客戶端代碼也更容易閱讀,當一個類需要多個帶有相同簽名的構造器時,就用靜態工廠方法代替構造器,並且慎重地選擇名稱以便突出它們之間的區別

  • 優勢二:不必在每次調用它們的時候都創建一個新對象

    靜態工廠方法能夠爲重複的調用返回相同對象,這樣有助於類總能嚴格控制在某個時刻哪些實例應該存在,這種類被稱作實例受控的類

  • 優勢三:可以返回原返回類型的任何子類型的對象

    公有的靜態工廠方法所返回的對象的類不僅可以是非公有的,而且該類還可以隨着每次調用而發生變化,這取決於靜態工廠方法的參數值,只要是已聲明的返回類型的子類型,都是允許的。爲了提升軟件的可維護性和性能,返回對象的類也可能隨着發行版本的不同而不同

  • 優勢四:在創建參數化類型實例的時候,它們使代碼變得更加簡潔

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

靜態工廠方法的主要缺點如下:

  • 缺點一:類如果不含公有的或者受保護的構造器,就不能被子類化
  • 缺點二:它們與其他的靜態方法實際上沒有任何區別

第2條:遇到多個構造器參數時要考慮用構建器

靜態工廠和構造器有個共同的侷限性:它們都不能很好地擴展到大量的可選參數

可以採用重疊構造器模式,在這種模式下,提供第一個只有必要參數的構造器,第二個構造器有一個可選參數,第三個有兩個可選參數,以此類推,最後一個構造器包含所有可選參數

public class NutritionFacts {

    private final int servingSize; // required
    private final int servings; // required
    private final int calories; // optional
    private final int fat; // optional
    private final int sodium; // optional
    private final int carbohydrate; // optional

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}

當有許多參數的時候,使用重疊構造器模式會使客戶端代碼很難編寫,並且較難以閱讀

當遇到許多構造器參數的時候,還有第二種代替方法,即JavaBeans模式,在這種模式下,調用一個無參構造器來創建對象,然後調用setter方法來設置每個必要的參數,以及每個相關的可選參數:

public class NutritionFacts {

    // Parameters initialized to default values(if any)
    private int servingSize = -1; // required
    private int servings = -1; // required
    private int calories = 0; // optional
    private int fat = 0; // optional
    private int sodium = 0; // optional
    private int carbohydrate = 0; // optional

    public NutritionFacts() {
    }

    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    public void setServings(int servings) {
        this.servings = servings;
    }

    public void setCalories(int calories) {
        this.calories = calories;
    }

    public void setFat(int fat) {
        this.fat = fat;
    }

    public void setSodium(int sodium) {
        this.sodium = sodium;
    }

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }
}

遺憾的是,JavaBeans模式自身有着很嚴重的缺點,因爲構造過程被分到了幾個調用中,在構造過程中JavaBean可能處於不一致的狀態,類無法僅僅通過檢驗構造器參數的有效性來保證一致性,試圖使用處於不一致狀態的對象,將會導致失敗,這種失敗調試起來十分困難,另外需要付出額外的努力來確保它的線程安全

第三種替代方法,既能保證像重疊構造器模式那樣的安全性,也能保證像JavaBeans模式那麼好的可讀性,這就是Builder模式的一種形式。不直接生成想要的對象,而是讓客戶端利用所有必要的參數調用構造器(或者靜態工廠),得到一個builder對象,然後客戶端在builder對象上調用類似於setter的方法,來設置每個相關的可選參數,最後,客戶端調用無參的build方法來生成不可變的對象,這個builder是它構建的類的靜態成員類,示例如下:

// Builder Pattern
public class NutritionFacts {

    // Parameters initialized to default values(if any)
    private final int servingSize; // required
    private final int servings; // required
    private final int calories; // optional
    private final int fat; // optional
    private final int sodium; // optional
    private final int carbohydrate; // optional

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories = 0; // optional
        private int fat = 0; // optional
        private int sodium = 0; // optional
        private int carbohydrate = 0; // optional

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int calories) {
            this.calories = calories;
            return this;
        }

        public Builder fat(int fat) {
            this.fat = fat;
            return this;
        }

        public Builder sodium(int sodium) {
            this.sodium = sodium;
            return this;
        }

        public Builder carbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    public NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

NutritionFacts是不可變的,所有的默認參數值都單獨放在一個地方,builder的setter方法返回builder本身,以便可以把調用鏈接起來,客戶端代碼如下:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();

builder像個構造器一樣,可以對其參數強加約束條件,build方法可以檢驗這些約束條件,將參數從builder拷貝到對象中之後,並在對象域而不是builder域中對它們進行檢驗,如果違反了任何約束條件,build方法就應該拋出IllegalStateException

與構造器相比,builder優勢在於,builder可以有多個可變(varargs)參數,構造器就像方法一樣,只能有一個可變參數

Builder模式十分靈活,可以利用單個builder構建多個對象,builder的參數可以在創建對象期間進行調整,也可以隨着不同的對象而改變。builder可以自動填充某些域,例如每次創建對象時自動增加序列號

Builder模式也有它自身的不足,爲了創建對象,必須先創建它的構造器,這會有一定的開銷。Builder模式還比重疊構造器模式更加冗長,因此它只在有很多參數的時候才使用,比如4個或更多個參數。如果將來需要添加參數,最好一開始就使用構建器

第3條:用私有構造器或者枚舉類型強化Singleton

Singleton指僅僅被實例化一次的類,Singleton通常被用來代表那些本質上唯一的系統組件,比如窗口管理器或文件系統

實現Singleton,公有的成員是個靜態工廠方法:

// Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { ... }
}

公有域方法的主要好處在於,組成類的成員的聲明很清楚地表明瞭這個類是一個Singleton,公有的靜態域是final的,所以該域將總是包含相同的對象引用

如果要實現Singleton類是可序列化的(Serializable),僅僅在聲明中加上“implements Serializable”是不夠的,還必須提供一個readResolve方法,否則每次反序列化一個序列化的實例時,都會創建一個新的實例

// readResolve method to preserve singleton property
private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator
    return INSTANCE;
}

從Java 1.5發行版本起,實現Singleton只需編寫一個包含單個元素的枚舉類型:

// Enum Singleton - the preferred approach
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}

這種方法在功能上與公有域方法相近,但是它更簡潔,無償地提供了序列化機制,絕對防止多次實例化,即使是在面對複雜的序列化或者反射攻擊的時候

第4條:通過私有構造器強化不可實例化的能力

讓這個類包含私有構造器,卻不允許被實例化

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

第5條:避免創建不必要的對象

對於同時提供了靜態工廠方法和構造器的不可變類,通常可以使用靜態工廠方法而不是構造器,以避免創建不必要的對象

除了可以重用不可變的對象之外,也可以重用那些已知不會被修改的可變對象

示例:

public class Person {

    private final Date birthDate;

    public boolean isBabyBoomer() {
        // Unnecessary allocation of expensive object
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= 0 &&
                birthDate.compareTo(boomEnd) < 0;
    }

}

isBabyBoomer每次調用的時候,都會新建一個Calendar、一個TimeZone和兩個Date實例,這是不必要的,改進如下:

public class Person {

    private final Date birthDate;

    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 &&
                birthDate.compareTo(BOOM_END) < 0;
    }

}

第6條:消除過期的對象引用

下例程序:

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.copyOf(elements, 2 * size + 1);
        }
    }

}

這段程序有一個“內存泄漏”,隨着垃圾回收器活動的增加,或者由於內存佔用的不斷增加,程序性能的降低會逐漸表現出來

如果一個棧先是增長,然後再收縮,那麼,從棧中彈出來的對象將不會被當作垃圾回收,即使使用棧的程序不再引用這些對象,它們也不會被回收,這是因爲,棧內部維護着對這些對象的過期引用(obsolete reference)。所謂的過期引用,是指永遠也不會再被解除的引用

這類問題的修復方法很簡單:一旦對象引用已經過期,只需清空這些引用即可

public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

清空過期引用的另一個好處是,如果它們以後又被錯誤地解除引用,程序會立即拋出NullPointerException異常,而不是悄悄地錯誤運行下去

清空對象引用應該是一種例外,而不是一種規範行爲,消除過期引用最好的方法是讓包含該引用的變量結束其生命週期

一般而言,只要類是自己管理內存,就應該警惕內存泄漏問題,一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空

內存泄漏的另一個常見來源是緩存,如果在緩存之外存在對某個項的鍵的引用,該項就有意義,那麼就可以用WeakHashMap代表緩存;當緩存中的項過期之後,它們就會自動被刪除,只有當所有的緩存項的生命週期是由該鍵的外部引用而不是由值決定時,WeakHashMap纔有用處

內存泄漏的第三個常見來源是監聽器和其他回調,如果實現了一個API,客戶端在這個API中註冊回調,卻沒有顯示地取消註冊,那麼除非採取某些動作,否則它們就會積聚,確保回調立即被當作垃圾回收的最佳方法是隻保存它們的弱引用(weak reference),例如,只將它們保存成WeakHashMap

第7條:避免使用終結方法

終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的

終結方法的缺點在於不能保證會被及時地執行。從一個對象變得不可到達開始,到它的終結方法被執行,所花費的這段時間是任意長的

Java語言規範不僅不保證終結方法會被及時執行,而且根本就不保證它們會被執行,所以不應該依賴終結方法來更新重要的持久狀態,例如依賴終結方法來釋放共享資源(比如數據庫)上的永久鎖

正常情況下,未被捕獲的異常將會使線程終止,並打印出棧軌跡(Stack Trace),但是,如果異常發生在終結方法之中,則不會如此,甚至連警告都不會打印出來

使用終結方法還會有一個非常嚴重的(Severe)性能損失

如果類的對象中封裝的資源(例如文件或者線程)確實需要終止,只需提供一個顯式的終止方法,並要求該類的客戶端在每個實例不再有用的時候調用這個方法。該實例必須記錄下自己是否已經被終止了:顯式的終止方法必須在一個私有域中記錄下“該對象已經不再有效”,如果這些方法是在對象已經終止之後被調用,其他的方法就必須檢查這個域,並拋出IllegalStateException異常

顯式終止方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法

顯式的終止方法通常與try-finally結構結合起來使用,以確保及時終止。在finally子句內部調用顯式的終止方法,可以保證即使在使用對象的時候有異常拋出,該終止方法也會執行:

Foo foo = new Foo(...);
try {
    ...
} finally {
    foo.terminate();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章