通常來講,最好能重用對象而不是在每次須要的時候就建立一個相同功能的新對象。重用的方式既快速,有流行。若是對象是不可變(immutable)的(第17項),那麼就能重複使用它。java
做爲一個極端的反面例子,考慮下面的語句:正則表達式
String s = new String("bikini"); // DON'T DO THIS!
該語句每次被執行的時候都建立一個新的String實例,可是這些對象的建立並不都是必要的。傳遞給String構造器的參數("bikini")
自己就是一個String實例,功能方面等同於構造器建立的全部對象。若是這種方法是用在一個循環中,或者是在一個被頻繁調用的方法中,就會建立出成千上萬的沒必要要的String實例。數據庫
改進後的版本以下:express
String s = "bikini";
這個版本只用了一個String實例,而不是每次執行時都建立一個新的實例。除此以外,它能夠保證,對於全部在同一臺虛擬機中運行的代碼,只要它們包含相同的字符串字面常量,該對象就會被重用 [JLS, 3.10.5]。緩存
對於同時提供了靜態工廠方法(第1項)和構造器的不可變類,一般可使用靜態工廠方法而不是構造器,這樣能夠常常避免建立沒必要要的對象。例如,這個靜態工廠方法Boolean.valueOf(String)
老是優先於在Java 9中拋棄的構造器 Boolean(String)
。構造函數必須在每次調用時建立一個新對象,而工廠方法從不須要這樣作,也不會在實踐中。除了重用不可變對象以外,若是你知道它們不會被修改,你還能夠重用可變對象。安全
有些對象的建立比其餘對象的代價大,若是你須要反覆建立這種代價大的對象,建議將其緩存起來以便重複使用。不幸的是,當你建立這樣一個對象時,並不老是很明顯。假設你想編寫一個方法來肯定一個字符串是不是一個有效的羅馬數字。使用正則表達式是最簡單的方法:ide
// 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實例的代價很大,由於它須要將正則表達式編譯爲有限狀態機(because it requires compiling the regular expression into a 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能夠顯着提升性能。在個人機器上,原始版本在8個字符的輸入字符串上須要1.1μs,而改進版本須要0.17μs,這是6.5倍的速度。不只提升了性能,並且功能更加明瞭。爲不可見的Pattern實例建立一個靜態的final字段,咱們能夠給它一個名字,它比正則表達式自己更具備可讀性。測試
若是初始化包含改進版本的isRimanNumberal方法的類時,可是從不調用該方法,則不須要初始化字段ROMAN。在第一次調用isRimanNumberal方法時,能夠經過延遲初始化字段(第83項)來消除使用時未初始化的影響,但不建議這樣作。延遲初始化的作法一般都有一個狀況,那就是它會把實現複雜化,從而致使沒法測試它的性能改進狀況。
當一個對象是不可變的,那麼就能夠安全地重用它,可是在其餘狀況下,它並非那麼明顯,甚至違反直覺。這時候能夠考慮使用適配器 [Gamma95],也稱爲視圖。適配器是委託給支持對象的對象(An adapter is an object that delegates to a backing object),它提供一個備用接口。由於適配器的狀態不超過其支持對象的狀態,因此不須要爲給定對象建立一個給定適配器的多個實例。
例如,Map接口的keySet方法返回Map對象的Set視圖,該視圖由Map中的全部鍵組成。看起來,彷佛每次調用keySet都必須建立一個新的Set實例,可是對給定Map對象上的keySet的每次調用均可能返回相同的Set實例。儘管返回的Set實例一般是可變的,但全部返回的對象在功能上都是相同的:當其中一個返回的對象發生更改時,全部其餘對象也會發生更改,由於它們都由相同的Map實例支持。雖然建立keySet視圖對象的多個實例在很大程度上是無害的,但沒必要要這樣作,而且這樣作沒有任何好處。
建立沒必要要的對象的另外一種方式是自動裝箱,它容許程序猿將基本類型和裝箱基本類型(Boxed Primitive Type)混用,按需自動裝箱和拆箱。自動裝箱使得基本類型和裝箱基本類型之間的差異變得模糊起來,可是並無徹底消除。它們在語義上還有微妙的差異,在性能上也有着比較明顯的差異(第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,意味着程序構造了大約 2^31 個多餘的Long實例(大約每次往Long sum中增長long時構造一個實例)。將sum的聲明從Long改爲long,在個人機器上運行時間從43秒減小到了6秒。結論很明顯:要優先使用基本類型而不是裝箱基本類型,要小心無心識的自動裝箱。
不要錯誤地認爲本項所介紹的內容暗示着「建立對象的代價很是昂貴,咱們就應該儘量地避免建立對象」。相反,因爲小對象的構造器只作不多量的顯示工做,因此,小對象的建立和回收動做是很是廉價的,特別是在現代的JVM實現上更是如此。經過建立附加的對象,提高程序的清晰性、簡潔性和功能性,這一般是件好事。
反之,經過維護本身的對象池(object pool)來避免建立對象並非一種好的作法,除非池中的對象是很是重量級的。真正正確使用對象池的經典對象示例就是數據庫鏈接池。創建數據庫鏈接的代價是很是昂貴的,所以重用這些對象很是有意義。可是,一般來講,維護本身的對象池一定會把代碼弄得很亂,同時增長內存佔用,並且會影響性能。現代的JVM實現具備高度優化的垃圾回收器,其性能很容易就會超太輕量級對象池的性能。
與本項對應的是第50項的「保護性拷貝」的內容。該項說得是:你應該重用已經存在的對象,而不是去建立一個新的對象。然而第50項說的是:你應該建立一個新的對象而不是重用一個已經存在的對象。注意,在提倡使用保護性拷貝的時候,因重用對象而付出的代價要遠遠大於因建立重複對象而付出的代價。必要時若是沒能實施保護性拷貝,將會致使潛在的錯誤和安全漏洞,而沒必要要地建立對象則只會影響程序的風格和性能。