《Effective Java》學習筆記 第二章 建立和銷燬對象

第二章 建立和銷燬對象

什麼時候以及如何建立對象,什麼時候以及如何避免建立對象,如何確保他們可以適時地銷燬,以及如何管理對象銷燬以前必須進行的各類清理動做。

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

通常在某處獲取一個類的實例最經常使用的方法是提供一個共有的構造器,還有一種方法,就是提供一個共有的 靜態工廠(static factory method),他只是一個返回類的實例的靜態方法。

例:java

public static Boolean valueOf(boolean b){
     return b ? Boolean.TRUE:Boolean.FALSE;
 }
注意,靜態工廠方法與設計模式中的工廠方法模式不一樣。類能夠經過靜態工廠方法來提供給它的客戶端,而不是經過構造器,提供靜態工廠方法而不是公有的構造器,這樣作具備幾大優點:
  • 靜態工廠方法,它們有名稱
    例如構造器BIgInteger(int,int,Random)返回的BigInteter可能爲素數,若是用名爲BigInteger.probablePrime的靜態工廠方法來表示,顯然更爲清楚。
  • 沒必要在每次調用它們的時候都建立一個新的對象程序員

    這使得不可變類可使用預先構建好的實例,或者將構建好的實例緩存起來,進行重複利用,從而避免常見沒必要要的重複對象,由於程序常常請求建立相同的對象,那麼建立對象的代價會很高。Boolean.valueOf(boolean)方法說明了這項技術。靜態工廠方法也常常用於實現單例模式。
  • 它們能夠返回原返回類型的任何子類型的對象

靈活的靜態工廠方法構成了服務提供者框架(Service Provider FrameWork)的基礎,例如JDBC。數據庫

對於JDBC,Connection就是它的服務接口,DriverManager.registerDriver是提供者註冊API,DriverManager.getConnection是服務訪問API,Driver就是服務提供者接口。設計模式

例,下列簡單的實現包含了一個服務提供者接口和一個默認提供者:數組

/**
 * Created by Newtrek on 2017/10/31.
 * 服務提供者接口
 */
public interface Provider {
//    提供服務實例,用服務接口返回
    Service newService();
}
/**
 * Created by Newtrek on 2017/10/31.
 * 服務接口
 */
public interface Service {
    // TODO: 2017/10/31  服務特有的方法 寫在這兒
}
/**
 * Created by Newtrek on 2017/10/31.
 */
public class Services {
//    構造保護
    private Services(){}
//    provider映射表,保存註冊的Provider
    private static final Map<String ,Provider> providers=new ConcurrentHashMap<>();
//    默認provider的名字
    public static final String DEFAULT_PROVIDER_NAME="<def>";
//    註冊默認的Provider
    public static void registerDefaultProvider(Provider p){
        registerProvider(DEFAULT_PROVIDER_NAME,p);
    }
//    註冊provider
    public static void registerProvider(String name,Provider p){
        providers.put(name,p);
    }

    /**
     * 靜態工廠方法返回Service實例
     */
    public static Service newInstance(){
        return newInstance(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();
    }
}
  • 在建立參數化類型實例的時候,它們是代碼變得更加簡潔

例:假設HashMap提供了這個靜態工廠緩存

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

那麼就能夠用下面簡潔的代碼獲取實例了。框架

Map<String,List<String>> m=HashMap.newInstance();

把這些方法放在本身的工具類中是很實用的。不過如今java7,java8已經實現了HashMap構造的類型參數推測dom

缺點

  • 類若是不含公有的或者受保護的構造器,就不能被子類化
  • 它們與其餘的靜態方法實際上沒有任何區別ide

    • 在API文檔中,它們沒有像構造器那樣在API文檔中明確標識出來,由於,對於提供了靜態工廠方法二不是構造器的類來講,要想查明如何實例化一個類,這是很是困難的。能夠經過在類或者接口註釋中關注靜態工廠,並遵照標準的命名習慣,能夠彌補這一劣勢。下面是靜態工廠方法的一些慣用名稱。工具

      • valueOf:其實是類型轉換方法。
      • of:valueOf的一種更爲簡潔的代替
      • getInstance:返回的實例是經過方法的參數來描述的,可是不可以說與參數具備一樣的值。對於Singleton來講,該方法沒有參數,並返回惟一的實例。
      • newInstance:像getInstance同樣,但newInstance可以確保返回的每一個實例都與全部其它實例不一樣。
      • getType:像getInstance同樣,可是在工廠方法處於不一樣的類中的時候使用。Type表示工廠方法所返回的對象類型。
      • newType:像newInstance同樣,可是在工廠方法處於不一樣的類中的時候使用,Type表示工廠方法所返回的對象類型。
簡而言之,靜態工廠方法和公有構造器都各有用處,咱們須要理解他們各自的長處。靜態工廠一般更加合適,所以切忌第一反應就是提供公有的構造器,而不先考慮靜態工廠。

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

這個就是Builder設計模式

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

這個是單例模式的注意事項,選擇最好的單例模式實現

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

有時候須要編寫一些只包含靜態方法和靜態域的類,這些類的名聲很很差,由於有些人在面向對象的語言中濫用這樣的類來編寫過程化的程序,儘管如此,他們也確實有它們的好處,好比常見的工具類java.lang.Math等,都是這樣。方正這樣不能夠實例化的類,最好把他的構造器設置爲私有。

例如:

public class UtilityClass{
  private UtilityClass(){
      throw new AssertionError();
  }
}

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

通常來講,最好能重用對象而不是再每次須要的時候就建立一個相同功能的新對象,若是對象是不可變的,他就始終能夠被重用。
簡單的例子:字符串

String s = new String("stringtest");//不要這樣作,由於該語句每次執行的時候都會建立一個新的String實例,不必
// 改進後的版本,這個版本只用了一個String實例,而不是每次執行的時候都建立一個新的實例。對於再同一臺虛擬機中運行的代碼,只要它們包含相同的字符串自字面常量,該對象就能夠被重用。
String s = "stringtest";

對於同時提供了靜態方法和構造器方法的不可變類,一般可使用靜態工廠方法而不是構造器,以免建立沒必要要的對象。

優先使用基本類型,而不是裝箱基本類型,小心無心識的自動裝箱。

也不要錯誤地認爲本條目暗示着「建立對象的代價很是昂貴,咱們應該儘量地避免建立對象」,相反,因爲小對象地構造器製做不多量地顯示工做,因此,小對象地建立和回收動做是很是廉價地。

經過維護本身的對象池來避免建立對象比不是一種好的作法,除非池中的對象是很是重量級的,通常數據庫鏈接池經常使用。

6 消除過時的對象引用

不要覺得Java有垃圾回收機制,能自動管理內存,自動回收垃圾,就能夠無論了,其實否則。
內存泄漏的例子

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_CAPACITY = 16;

    public Stack(){
        elements = new Object[DEFAULT_CAPACITY];
    }

    public void push(Object object){
        ensureCapacity();
        elements[size++] = object;
    }

    public Object pup(){
        if (size == 0){
            throw  new EmptyStackException();
        }
        return elements[size--];
    }

    private void ensureCapacity(){
        if (elements.length == size){
            elements = Arrays.copyOf(elements,size*2+1);
        }
    }

}

這段程序並無明顯的錯誤,若是是棧先是增加,而後再收縮,那麼,從棧中彈出來的對象將不會被看成垃圾回收,由於裏面的數組裏引用着它,棧內部維護着這些對象的過時引用,過時引用就是指永遠也不會再被解除的引用。

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

不必對於每個對象引用,一旦程序再也不用到它,就把它清空。清空對象引用應該是一種例外,而不是一種規範行爲,消除過時引用最好的辦法是讓包含該對象的變量結束其生命週期。通常而言,只要類是本身管理內存,程序員就應該警戒內存泄漏問題,一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。

內存泄漏的另外一個常見來源是緩存,一旦你把對象放在緩存中,他就很容易被遺忘掉,從而使得它再也不有用以後很長一段時間內仍然留在緩存中。能夠用WeakHashMap

內存泄漏的第三個常見來源是監聽器和其餘回掉,通常都要取消註冊,或者用弱引用

內存泄漏一般不會表現成明顯的失敗,因此他們能夠再一個系統中存在不少年,每每經過仔細檢查代碼,藉助於Heap刨析工具才能發現內存泄漏問題。

7 避免使用終結方法

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

C++的析構器是回收一個對象所佔用資源的常規方法,是構造器所必須的對應物,也能夠用來回收其餘的非內存資源,而在Java中,通常用try-finally塊來完成相似的工做

相關文章
相關標籤/搜索