Effective Java 第三版讀書筆記——條款6:避免建立不必要的物件
通常來講,重用一個物件比建立一個功能相同的物件更加合適。重用速度更快,並且更接近現代的程式碼風格。如果物件是不可變的(條款 17),它總是可以被重用。
考慮一個極端的例子:
String s = new String("bikini"); // DON'T DO THIS!
這個語句每次執行時都會建立一個新的 String 例項,而這些例項的建立都是不必要的。如果這種用法發生在迴圈或者頻繁呼叫的方法中,就會建立數百萬個毫無必要的 String 例項。
改進後的版本如下:
String s = "bikini";
該版本使用單個 String 例項,而不是每次執行時建立一個新例項。
一些物件的建立可能會比其他物件的建立昂貴很多。 如果要重複使用這樣一個“昂貴的物件”,建議將其快取起來以便重用。 不幸的是,建立這樣一個物件並不總是很直觀的。 假設你想寫一個方法來確定一個字串是否是一個合法的羅馬數字。 下面是使用正則表示式完成此操作的最簡單方法:
// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$" );
}
這個實現的問題在於它依賴於 String.matches
方法。 雖然 String.matches
是檢查字串是否與正則表示式匹配的最簡單方法,但它不適合在效能臨界的情況下重複使用。 因為它在內部為正則表示式建立一個 Pattern
例項,並且只使用一次,之後這個 Pattern
例項就會被 JVM 進行垃圾回收。 建立 Pattern
例項是昂貴的,因為它需要將正則表示式編譯成有限狀態機(finite state machine)。
為了提高效能,將正則表示式顯式編譯為一個 Pattern
例項(不可變)並且快取它,在 isRomanNumeral
方法的每個呼叫中重複使用相同的例項:
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
如果經常呼叫,改進之後的 isRomanNumeral
會使效能得到顯著提升。 而且將不可見的 Pattern
例項顯式建立允許我們給它起一個名字,這個名字通常比正則表示式本身更具可讀性。
另一種建立不必要的物件的方式是自動裝箱(autoboxing),它允許程式設計師混用基本型別和包裝的基本型別,根據需要自動裝箱和拆箱。 自動裝箱模糊不清,但不會消除基本型別和裝箱基本型別之間的區別。 考慮下面這個計算 int
範圍內正整數總和的方法。 要做到這一點,程式必須使用 long
型別,因為 int
型別不足以儲存最後的結果:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
這個程式的結果是正確的,但由於寫錯了一個字元,執行的結果要比實際慢很多。變數 sum
被宣告成了 Long
而不是 long
,這意味著程式構造了大約
個不必要的Long
例項(每次往 Long
型別的 sum
變數中增加一個 long
型別的 i
)。把 sum
變數的型別由 Long
改為 long
會使效能得到很大提升。這個教訓很明顯:優先使用基本型別而不是包裝的基本型別,也要注意無意識的自動裝箱。
這個條目不應該被誤解為暗示物件建立是昂貴的,應該避免建立物件。 相反,建立和回收小的物件非常廉價,構造器只會做很少的工作,尤其在現代 JVM 實現上。 建立額外的物件以增強程式的清晰性,簡單性或功能性通常是件好事。