Effective Java 學習筆記——第二章

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

靜態工廠方法的優勢:

1. 更加語義化

較之於new A() 的構造方法,靜態工廠方法有函數名稱,函數的功能描述更加清晰;

2. 不必在每次調用時都創建新的對象(即允許單例)

3. 可以返回原返回類型的任何子類型對象

可以根據傳入的參數、所需的功能,返回所需要的子類型對象。
如下代碼,傳入一個對象後,靜態工廠方法根據傳入對象的長度來返回不同的枚舉類型:小於等於64個元素,返回RegalarEumSet實例;大於64個元素,返回JumboEnumSet實例。

  public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable {
    EnumSet(Class<E>elementType, Enum[] universe) {
    }
    //RegularEnumSet與JumboEnumSet均爲EnumSet的子類
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }
}

4. 使創建對象更加簡潔

對比如下:

//常規實例化方式
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();

靜態工廠方法的劣勢:

1. 類不含public或protected構造方法時,無法被其他類繼承

2. 與其他靜態方法沒有本質區別

不能夠突出靜態工廠方法的構造特點。




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

遇到多個構造器參數,且有必需與可選參數時,向對象傳入屬性的方法有如下三種(以下例子中,name、age爲必需屬性,sex、height、QQ爲可選屬性):

1. 重載構造器

public People(String name,int age){...}
public People(String name,int age, char sex){...}
public People(String name,int age, char sex, int height){...}
public People(String name,int age, char sex, int height, int QQ){...}

由上可見,重載構造器的方法過於複雜,不夠語義化,且容易出現錯誤(如age與height參數輸入反了,並不會產生錯誤提示)不適於多個參數。


2. JavaBeans

JavaBeans即通過set方法對屬性進行逐一注入,相對語義化一些,但仍然非常繁瑣,且容易使JavaBean處於不一致狀態(如一個只set了A屬性,一個只設置了B屬性,這兩個實例不一致,不能保證通過該類的同一個構造器保證構造出來的對象是屬性相同的),以及阻止了把類做成不可變的可能


3. Builder

public class People
{
    private final String name;
    private final int age;
    private final char sex;
    private final int height;
    private final int QQ;

    public static class Builder
    {
        private final String name;
        private final int age;

        private final char sex;
        private final int height;
        private final int QQ;

        public Builder( String name, int age ){
            this.name = name;
            this.age= age;
        }

        public Builder sex( char sex ){
            this.sex = sex ;
            return this;
        }

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

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

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

    private People ( Builder builder ){
        height= builder.height;
        sex = builder.sex;
        QQ = builder.QQ;
    }
}

//call code
People people = new People.Builder( "zhangsan", 8 ).height(150).sex('m').build();

如上所示,對於大量可選參數而言,Builder模式(構建器模式)更加簡便,代碼邏輯更加清晰。而對於可選參數較少的情況,Builder模式會顯得繁雜一些。




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

本條有部分內容爲擴展內容,參考來源http://www.cnblogs.com/blogofcookie/p/5793865.html

1. 常見的4種單例模式實現方案:

①餓漢式

public class SingletonDemo01 {
    //類初始化時,立刻加載這個對象(沒有延時加載的優勢)。線程是天然安全
    private static SingletonDemo01 instance = new SingletonDemo01();
    private SingletonDemo01(){}
    //方法沒有同步,線程安全,調用效率高。但是,不能延時加載
    public static SingletonDemo01 getInstance(){
        return instance;
    }
}

②懶漢式

public class SingletonDemo02 {
    //類加載時,不初始化對象(延時加載:資源利用率高)
    private static SingletonDemo02 instance;
    private SingletonDemo02(){}         
    //synchronized 防止併發量高的時候,出現多個對象方法同步,調用效率低,
    public static synchronized SingletonDemo02 getInstance(){
        if(instance==null){//真正用的時候才加載
            instance = new SingletonDemo02();
        }
        return instance;
    }
}

③靜態內部類實現(實質是利用了靜態內部類只在調用時初始化一次的特性,保證線程安全與延遲加載)

public class SingletonDemo04 {
    //類加載時靜態內部類不會加載,只有調用getInstance方法時,纔會加載(實現延時加載)
    private static class SingletonClassInstance {
        private final static SingletonDemo04 instance = new SingletonDemo04();
    }
    private SingletonDemo04() {
    }
    // 線程安全,方法不同步,調用效率提高
    public static SingletonDemo04 getInstance() {
        return SingletonClassInstance.instance;
    }
}

④枚舉式

public enum SingletonDemo05 {
    INSTANCE;// 這個枚舉元素,本身就是單例模式
    // 添加自己需要的操作
    public void singletonOperation() {...}
}

以上四種方案對比如下:
餓漢式:線程安全,調用效率高(因爲無鎖),不能延遲加載,會被反射、序列化破壞單例安全;
懶漢式:線程安全,調用效率低(因爲有鎖),可以延遲加載,會被反射、序列化破壞單例安全;
靜態內部類式:線程安全,調用效率高,可以延遲加載,會被反射、序列化破壞單例安全;
枚舉式:線程安全,調用效率高,不能延遲加載,保證絕對單例安全;

選用方案:
單例對象佔用資源少時,可以不延遲加載,選用枚舉式;
單例對象佔用資源多時,必須延遲加載,選用靜態內部類式;


2. 破壞單例安全的方式

①克隆

只有實現Cloneable接口的類纔可以使用clone方法,該方法從內存直接copy對象,繞開了構造器。所以,單例模式的類切勿實現Cloneable接口。

②反射

    public class Client {
        public static void main(String[] args) throws Exception {
            SingletonDemo06 s1 = SingletonDemo06.getInstance();
            //使用反射方式直接調用私有構造器
            Class<SingletonDemo06> clazz = (Class<SingletonDemo06>)Class.forName("com.bjsxt.singleton.SingletonDemo06");
            Constructor<SingletonDemo06> c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);//繞過權限管理,即在true的情況下,可以通過構造函數新建對象
            SingletonDemo06 s3 = c.newInstance();
            System.out.println(s1==s3);
        }
    }

通過反射機制,可調用私有構造器創建對象。
解決方案:修改構造器,在創建第二個實例時拋出異常。枚舉式無此問題。

③序列化

public class Client {
    public static void main(String[] args) throws Exception {
        SingletonDemo06 s1 = SingletonDemo06.getInstance();
        //通過反序列化的方式創建多個對象
        FileOutputStream fos= new FileOutputStream("d:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.close();
        fos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
        SingletonDemo06 s5= (SingletonDemo06) ois.readObject();
        System.out.println(s5==s1);
    }
}

在將對象持久化或遠程傳輸時會涉及序列化,序列化與反序列化會導致創建一個新的對象,破壞了單例模式。
解決方案:定義readResolve函數,返回已創建的對象。枚舉式無此問題。

private Object readResolve() {
        return Singleton.instance;
}



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

比如工具類,其所有方法均爲靜態方法,實例化對其沒有任何意義,所以應將其構造器私有化,以避免被錯誤實例化。

public class Utility{
    private Utility(){
        //內部調用構造器,拋出異常;外部禁止調用構造器。
        throws new AssertionError();
    }

    ...//其他靜態方法
}



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

1. 能夠重用對象的時候,就不要創建新的對象

for(int i=0;i<100;i++){
    String a="a";
    String b=new String("b");
}

如上循環,由於String不可變性,只產生了一個a實例,和100個b實例。
所以採用創建a對象時的創建方法。

再如

public class Person
{
    private final Date birthDate;

    //Donot do this
    public boolean isBabyBoomer()
    {
        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
                && birthData.compareTo( boomEnd )<0;
    }
}

如上代碼,isBabyBoomer被調用時,每次都會創建Calendar和Date對象(其中Calendar是大對象),消耗內存。
下面是對上面代碼的改進方案,採用靜態不可變屬性與靜態代碼塊實現。

//better implements
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();
    }

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

該設計中,無論該函數被調用多少次,都只產生了一個Calendar對象,節約空間與性能。更好的設計應是能夠延遲加載的。


2. 有靜態工廠方法時,不要使用構造器方法創建對象

原因同1。構造器方法每次被調用都會產生一個新對象,而靜態工廠方法則不一定,有時是爲避免創建不必要對象而設立的。


3. 注意自動裝箱與自動拆箱

如下代碼

for(Long sum=0,long i=0;i<Integer.MAX_VALUE;i++){
    sum+=i;
}

由於sum定義爲Long,在計算時Long會拆箱爲long,計算完後又會裝箱爲Long,循環多次後極大消耗性能。所以應定義爲基本數據類型long。


4. 關於對象池

小對象的創建與銷燬,隨着JVM的提升,已經非常完善。
對象池應關注大對象的反覆創建與銷燬,如數據庫連接池,關注的是Connection對象的創建與銷燬,該對象創建代價非常昂貴,因此重用這些對象非常有意義。




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

1. 類自己管理的內存,應警惕內存泄露問題

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 Object pop(){
        if(size==0){
            throw new EmptyStackException();
        }
        return elements[--size];
    }

    ...//其他函數
}

如上所示,由於Object[]長度不變,size只限定了能夠使用的範圍,這會導致範圍之外的元素變爲過期引用(永遠也不會再被解除的引用)。這就導致內存泄露。使用久了,會導致內存溢出。
解決上述問題也很簡單:

public Object pop(){
    if(size==0){
        throw new EmptyStackException();
    }
    Object[element]=null;//防止內存泄露
    return elements[--size];
}

2. 內存泄露的另一個來源是緩存

將強引用放入緩存中,在緩存失效之前,該強引用指向的對象不會被GC,佔用空間。


3. 內存泄露的第三個來源是監聽器和其他回調

確保回調立即被當做垃圾回收的最佳方法是隻保存它們的弱引用




第7條 避免使用終結方法

1. 終結方法的缺點

因爲終結方法的線程優先級很低,所以不能保證被及時執行。
終結方法對性能消耗嚴重。

2. 終結方法替代方案

顯式終結,如Connection的close方法。

3. 終結方法應用場景

爲本應顯示終結但並未調用顯示終結的對象充當安全網。
如Connection對象已經使用完畢,但並未顯式調用,此時可以使用終結方法來終結該對象,雖然可能很久以後纔會終結,但至少比不終結要好很多。
基本上,不建議使用終結方法,會帶來很多問題。

發佈了46 篇原創文章 · 獲贊 31 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章