Effective Java讀書筆記三:建立和銷燬物件(1-7)

衣舞晨風發表於2017-02-04

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

對於類而言,為了讓客服端獲得它的一個例項最常用的的一個方法就是提供一個公有的構造器。還有一種方法,類可以提供一個公有的靜態工廠方法(static factory method),它只是一個返回類例項的靜態方法。

通過靜態工廠方法構造物件的優勢:

  1. 靜態工廠方法與構造器不同的第一大優勢在於,它們有名稱,使客服端程式碼更加容易被閱讀。
  2. 不必在每次呼叫的它們的時候都建立一個新的物件(這個完全取決於具體的實現)。
  3. 它們可以返回原返回型別的任何子型別的物件。
    這種靈活性的一種應用:API可以返回物件,同時又不會使物件的類變成公有的。公有的靜態方法所返回的物件的類不僅可以是非公有的,而且該類還可以隨著每次呼叫而發生變化著取決於靜態工廠方法的引數值,只要是已宣告返回型別的子型別,都是允許的。
  4. 在建立引數化型別(也就是泛型,jdk1.5新特性)例項的時候,它們是的程式碼變得更加簡潔。
/**普通建立****/  
Map<String,List<String>> m=new HashMap<String,List<String>>;  
/**有了靜態方法過後***/  
Map<String,List<String>> m=HashMap.newInstance();  
//前提HashMap提供了這個靜態工廠方法  
 public static <k,v> HashMap<k,v> newInstance(){  
  return new HashMap<K,V>();  
}  

靜態工廠方法的主要缺點在於:

  1. 類如果不含有他的公有或者受保護的構造器,就不能被子類化(即被繼承)。
  2. 它們與其他靜態方法實際上沒有任何區別。

常用的靜態工廠名稱:valueOf,of,getInstance,newInstance,getType,newType.

第2條:遇到多個構造引數時要考慮用構建器(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屬性

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() { ... }  
    } 

就目前而言,這種方法在功能上和公有域方式相近,但是他更加簡潔更加清晰,擴充套件性更強也更加安全。

第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”); 

該語句每次被執行的時候都建立一個新的String例項,但是這些建立物件的動作全都是不必要的,傳遞給String構造器的引數(“stringette”)本身就是一個String例項,功能方面等同於構造器建立的物件。如果這種用法是在一個迴圈中,或者是在一個被頻繁呼叫的方法中,就會建立出很多不必要的String例項。
改進:

String s = “stringette”; 

改進後,只用一個String例項,而不是每次執行的時候都建立一個新的例項,而且,它可以保證,對於所有在同一虛擬機器中執行的程式碼,只要它們包含相同的字串字面常量,該物件就會被重用。

對於同時提供了靜態工廠方法和構造器的不可變類,通常可以使用靜態工廠方法而不是構造器,以避免建立不必要的物件。
例如,靜態工廠方法Boolean.valueOf(String)幾乎總是比構造器Boolean(String)好,構造器在每次被呼叫的時候都會建立一個新的物件,而靜態工廠方法則從來不要求這樣做,實際上也不會這樣做。

要優先使用基本型別而不是裝箱基本型別,要當心無意識的自動裝箱

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

這段程式算出的答案是正確的,但是比實際情況要更慢一些,只因為打錯了一個字元。變數sum被宣告成Long而不是long,這就意味著程式構造了大約2^31個多的Long例項。

Java共有9中基本型別,同別的語言有重要區別的是這9中型別所佔儲存空間大小與機器硬體架構無關,這使得Java程式有很強的可移植性,如下圖

這裡寫圖片描述

不要錯誤地認為“建立物件的代價非常昂貴,我們應該要儘可能地避免建立物件,而不是不建立物件”,相反,由於小物件的構造器只做很少量的工作,所以,小物件的建立和回收動作是非常廉價的,特別是在現代的JVM實現上更是如此。通過建立附加的物件,提升程式的清晰性、簡潔性和功能性,這通常也是件好事。

反之,通過維護自己的物件池來建立物件並不是一種好的做法,除非池中的物件是非常重量級的。真正正確使用物件池的典型物件示例就是資料庫連線池。建立資料庫連線的代價是非常昂貴的,因此重用這些物件非常有意義。

另外,在1.5版本里,對基本型別的整形包裝型別使用時,要使用形如 Byte.valueOf來建立包裝型別,因為-128~127的數會快取起來,所以我們要從緩衝池中取,Short、Integer、Long也是這樣。

第6條:消除過期的物件引用

儘管Java不像C/C++那樣需要手工管理記憶體資源,而是通過更為方便、更為智慧的垃圾回收機制來幫助開發者清理過期的資源。即便如此,記憶體洩露問題仍然會發生在你的程式中,只是和C/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);  
         }  
     } 

這段程式有一個“記憶體洩漏”問題,如果一個棧先是增長,然後再收縮,那麼,從棧中彈出來的物件不會被當做垃圾回收,即使使用棧的程式不再引用這些物件,它們也不會被回收。這是因為,棧內部維護這對這些物件的過期使用(obsolete reference),過期引用指永遠也不會被解除的引用。
修復的方法很簡單:一旦物件引用已經過期,只需要清空這些引用即可。對於上述例子中的Stack類而言,只要一個單元彈出棧,指向它的引用就過期了,就可以將它清空。

修改方式如下:

public Object pop() {  
         if (size == 0)   
             throw new EmptyStackException();  
         Object result = elements[--size];  
         elements[size] = null; //手工將陣列中的該物件置空  
         return result;  
     } 

Stock為什麼會有記憶體洩漏問題呢?
問題在於,Stock類自己管理記憶體。儲存池中包含了elements陣列(物件引用單元,而不是物件本身)的元素。陣列活動區域的元素是已分配的,而陣列其餘部分的元素是自由的。但是垃圾回收器並不知道這一點,就需要手動清空這些陣列元素。
一般而言,只要類是自己管理記憶體,就應該警惕記憶體洩漏問題。一旦元素被釋放掉,則該元素中包含的任何物件引用都應該被清空。

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

  1. 類是自己管理記憶體,如例子中的Stack類。
  2. 使用物件快取機制時,需要考慮被從快取中換出的物件,或是長期不會被訪問到的物件。
  3. 事件監聽器和相關回撥。使用者經常會在需要時顯示的註冊,然而卻經常會忘記在不用的時候登出這些回撥介面實現類。

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

Java的語言規範中並沒有保證終結方法會被及時的執行,甚至都沒有保證一定會被執行。即便開發者在code中手工呼叫了System.gc和System.runFinalization這兩個方法,這僅僅是提高了finalizer被執行的機率而已。還有一點需要注意的是,被過載的finalize()方法中如果丟擲異常,其棧幀軌跡是不會被列印出來的。

《Effective Java中文版 第2版》PDF版下載:
http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出處:http://blog.csdn.net/jiankunking

相關文章