Java高效程式設計之一【建立和銷燬物件】

九天高遠發表於2013-08-06

一、考慮用靜態工廠方法替代建構函式

代表實現:java.util.Collection Framework

Boolean類的簡單例子:

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

優點:

1、與建構函式不同,靜態工廠方法具有名字。

一個類看起來需要多個建構函式,並且它們的執行特徵相同,應考慮使用靜態工廠方法來替代其中一個或多個建構函式,並且要慎重選擇它們的名字以明顯標示他們的不同。

2、與建構函式不同,他們每次呼叫的時候,不要求非得建立一個新的物件。

當一個程式要頻繁的建立相同的物件,並且建立物件的代價是昂貴的,這項技術可以極大地提高效能。

3、與建構函式不同,它們可以返回一個原返回型別的子型別的物件。

缺點:

1、類如果不含公有的或者受保護的建構函式,就不能被繼承。

2、它們與其他的靜態方法沒有任何區別。

使用靜態工廠方法要遵循命名習慣,其中兩個已經很流行了

valueOf : 非常有效的型別轉換符

getInstance : 返回例項,對於單例模式返回唯一的例項。

結論:建構函式和靜態工廠要合理選擇使用。

二、使用私有建構函式強化singleton屬性

實現singleton有兩種方法,這兩種方法都要把建構函式保持為私有的,並且提供一個靜態成員,以便能夠允許客戶訪問該類的唯一例項

1、帶有公有final域的方法

// Singleton with final field
public class Elvis{
public static final Elvis INSTANCE=new Elvis();

 private Elvis(){

        ……
}
         ……//Remained omitted
}

2、靜態工廠方法

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

 private Elvis(){

        ……
}
public static Elvis getInstance(){
              return INSTANCE;
}
         ……//Remained omitted
}

如果確定永遠是一個singleton,用第一種方法是有意義的,如果保留餘地,讓以後可以修改,使用第二種方法比價好。為了使一個singleton方法是可以序列化(serializable)的,僅僅在宣告中加上"implements Serializable"是不夠的。為了維護singleton性,必須加上一個readResolve方法,否則的話一個序列化的例項在反覆序列化的時候,都會導致建立一個新的例項。為了防止這種情況在readResolve方法:

// readResolve 方法保持單例屬性
private Object readResolve() throws ObjectStreamException{
/**
 *返回真正的Elvis,讓垃圾收集器注意假冒的Elvis
*/
}

三、透過私有建構函式強化不可例項化的能力

將一個類包含單個顯示的私有建構函式,則它就不可以被例項化了:

//不可例項化的實體類
public class UtilityClass {
//不能例項化的抑制預設建構函式
private UtilityClass(){
//該建構函式將永遠不能被呼叫
}
……//其餘的省略
}

四、避免建立重複的物件

當你重用一已有物件的時候,請不要建立新的物件,而同樣的,當你建立一個新的物件的時候,請不要重用一個已有物件。在提倡使用保護行複製的場合,因重用一個物件兒招致的代價要遠遠大於因建立重複物件而招致的代價。在要求保護性複製的情況下卻沒有實施保護性複製,將會導致錯誤和安全漏洞;而不必要的建立物件僅僅會影響程式的風格和效能。

五、消除過期的物件引用

 考慮下面棧實現的例子,你能否發現記憶體洩露的位置:

public class Stack { 
    private Object[] elements; 
    private int size = 0; 
 
    public Stack(int initialCapacity) { 
        this.elements = new Object[initialCapacity]; 
    } 
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) { Object[] oldElements = elements; elements = new Object[2 * elements.length + 1]; System.arraycopy(oldElements, 0, elements, 0, size); } } }

 從棧中彈出來的物件將不會當做垃圾回收,即使使用棧的客戶程式不再引用這些物件,它們將不會回收。這是因為,棧內部維護這對這些物件的過期引用(過期引用,即永遠也不會再被解除的引用),本例中凡是elements陣列活動區域之外的引用都是過期的,elements的活動區域是指下標小於size的那一部分。

 要想修復該問題很簡單,一旦物件引用已經過期,只需要清空這些引用即可。pop方法的修訂版如下:

public Object pop() { 
    if (size==0) 
        throw new EmptyStackException(); 
    Object result = elements[--size]; 
    elements[size] = null; // 刪除過期引用
    return result; 
} 

 “清空度物件引用”這樣的操作應該是一個例外,而不是一種規範行為。消除過期引用最好的辦法是重用一個本來已經包含物件引用的變數,或者讓這個 變數結束期宣告週期。何時清除一個引用,Stack哪方面的特性使得它遭受到了記憶體洩露的影響?

   根源在於Stack自己管理記憶體,儲存池包含了elements陣列(物件引用單元,而不是引用本身)的元素。陣列活動區域中的元素是已分配的,而陣列其餘部分的元素是自由的。但垃圾回收器不知道,需要程式設計師手動清空。

六、避免使用終結函式

顯示的終止方法通常與try-finally結構結合起來使用,以確保及時終止。

// try-finally 塊保證終結方法的執行
Foo foo = new Foo(...); 
try { 
    // 對fool執行一些必須操作
    ... 
} finally { 
    foo.terminate();  // 顯示終結方法
} 

另一種方法是“終結函式鏈(finalizeer chaining)”不會自動執行,如果一個類(不是Object類)有一個終結函式,並且只有一個子類改寫了終結函式,那麼子類的終結函式必須手工呼叫父類的終結函式。

 // 手工終結鏈
protected void finalize() throws Throwable { 
    try { 
        // 終結子類的狀態
... } finally { super.finalize(); } }

即使子類的終結過程中跑出一個異常,超類的終結函式也會被執行,反之亦然。
但是如果子類改寫了超累的終結函式,但是忘記呼叫超累的終結函式(或者有意不呼叫),會使得超類的終結函式永遠得不到執行。為了解決這一問題,我們使用匿名類的單個例項(即終結函式守衛者)來終結其外圍例項。

// 終結函式守衛者
public class Foo { 
   // 此物件的唯一目的是終結微微的Fool物件 
   private final Object finalizerGuardian = new Object() { 
      protected void finalize() throws Throwable { 
         // 終結外圍的 Foo 物件
         ... 
      } 
   }; // 匿名類
...
// 餘下省略 }

公有類Fool並沒有終結函式(除了它從Object中繼承了一個無關緊要的finalize之外),所以子類的終結函式是否呼叫super.finalize並不重要,對於每一個帶有終結函式的公有非final類,都應該考慮使用這項技術。

 

 

 

相關文章