Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。
6. 避免創建不必要的對象
在每次需要時重用一個對象而不是創建一個新的相同功能對象通常是恰當的。重用可以更快更流行。如果對象是不可變的(條目 17),它總是可以被重用。
作為一個不應該這樣做的極端例子,請考慮以下語句:
String s = new String("bikini"); // DON'T DO THIS!
語句每次執行時都會創建一個新的String實例,而這些對象的創建都不是必需的。String構造方法(“bikini”)
的參數本身就是一個bikini
實例,它與構造方法創建的所有對象的功能相同。如果這種用法發生在循環中,或者在頻繁調用的方法中,就可以毫無必要地創建數百萬個String實例。
改進后的版本如下:
String s = "bikini";
該版本使用單個String實例,而不是每次執行時創建一個新實例。此外,它可以保證對象運行在同一虛擬機上的任何其他代碼重用,而這些代碼恰好包含相同的字符串字面量[JLS,3.10.5]。
通過使用靜態工廠方法(static factory methods(項目1),可以避免創建不需要的對象。例如,工廠方法Boolean.valueOf(String)
比構造方法Boolean(String
)更可取,后者在Java 9中被棄用。構造方法每次調用時都必須創建一個新對象,而工廠方法永遠不需要這樣做,在實踐中也不需要。除了重用不可變對象,如果知道它們不會被修改,還可以重用可變對象。
一些對象的創建比其他對象的創建要昂貴得多。 如果要重復使用這樣一個“昂貴的對象”,建議將其緩存起來以便重復使用。 不幸的是,當創建這樣一個對象時並不總是很直觀明顯的。 假設你想寫一個方法來確定一個字符串是否是一個有效的羅馬數字。 以下是使用正則表達式完成此操作時最簡單方法:
// 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
實例是昂貴的,因為它需要將正則表達式編譯成有限狀態機(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();
}
}
如果經常調用,isRomanNumera
l的改進版本的性能會顯著提升。 在我的機器上,原始版本在輸入8個字符的字符串上需要1.1微秒,而改進的版本則需要0.17微秒,速度提高了6.5倍。 性能上不僅有所改善,而且更明確清晰了。 為不可見的Pattern實例創建靜態final修飾的屬性,並允許給它一個名字,這個名字比正則表達式本身更具可讀性。
如果包含isRomanNumeral
方法的改進版本的類被初始化,但該方法從未被調用,則ROMAN
屬性則沒必要初始化。 在第一次調用isRomanNumeral
方法時,可以通過延遲初始化( lazily initializing)屬性(條目 83)來排除初始化,但一般不建議這樣做。 延遲初始化常常會導致實現復雜化,而性能沒有可衡量的改進(條目 67)。
當一個對象是不可變的時,很明顯它可以被安全地重用,但是在其他情況下,它遠沒有那么明顯,甚至是違反直覺的。考慮適配器(adapters)的情況[Gamma95],也稱為視圖(views)。一個適配器是一個對象,它委托一個支持對象(backing object),提供一個可替代的接口。由於適配器沒有超出其支持對象的狀態,因此不需要為給定對象創建多個給定適配器的實例。
例如,Map接口的keySet方法
返回Map對象的Set視圖,包含Map中的所有key。 天真地說,似乎每次調用keySet都必須創建一個新的Set實例,但是對給定Map對象的keySet
的每次調用都返回相同的Set實例。 盡管返回的Set實例通常是可變的,但是所有返回的對象在功能上都是相同的:當其中一個返回的對象發生變化時,所有其他對象也都變化,因為它們全部由相同的Map實例支持。 雖然創建keySet
視圖對象的多個實例基本上是無害的,但這是沒有必要的,也沒有任何好處。
另一種創建不必要的對象的方法是自動裝箱(autoboxing),它允許程序員混用基本類型和包裝的基本類型,根據需要自動裝箱和拆箱。 自動裝箱模糊不清,但不會消除基本類型和裝箱基本類型之間的區別。 有微妙的語義區別和不那么細微的性能差異(條目 61)。 考慮下面的方法,它計算所有正整數的總和。 要做到這一點,程序必須使用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
,這意味着程序構造了大約231不必要的Long
實例(大約每次往Long
類型的 sum
變量中增加一個long
類型構造的實例),把sum
變量的類型由Long
改為long
,在我的機器上運行時間從6.3秒降低到0.59秒。這個教訓很明顯:優先使用基本類型而不是裝箱的基本類型,也要注意無意識的自動裝箱。
這個條目不應該被誤解為暗示對象創建是昂貴的,應該避免創建對象。 相反,使用構造方法創建和回收小的對象是非常廉價,構造方法只會做很少的顯示工作,,尤其是在現代JVM實現上。 創建額外的對象以增強程序的清晰度,簡單性或功能性通常是件好事。
相反,除非池中的對象非常重量級,否則通過維護自己的對象池來避免對象創建是一個壞主意。對象池的典型例子就是數據庫連接。建立連接的成本非常高,因此重用這些對象是有意義的。但是,一般來說,維護自己的對象池會使代碼混亂,增加內存占用,並損害性能。現代JVM實現具有高度優化的垃圾收集器,它們在輕量級對象上輕松勝過此類對象池。
這個條目的對應點是針對條目 50的防御性復制(defensive copying)。 目前的條目說:“當你應該重用一個現有的對象時,不要創建一個新的對象”,而條目 50說:“不要重復使用現有的對象,當你應該創建一個新的對象時。”請注意,重用防御性復制所要求的對象所付出的代價,要遠遠大於不必要地創建重復的對象。 未能在需要的情況下防御性復制會導致潛在的錯誤和安全漏洞;而不必要地創建對象只會影響程序的風格和性能。