? ? ? ? 這通常是很合適去重用一個單例而不是去再創(chuàng)建另一個每一次使用都相同的功能性對象。重用資源可以更快也更加流行。同時一個不可變得對象總是能夠被重用?。l目17)
? ? ? ?這里有一個極端的例子,思考下面一條語句:
? ? ?String?s?=?new?String("bikini"); // DON'T DO?THIS!
? ? ?這條語句每一次執(zhí)行都創(chuàng)建了一個新的String實例,同時沒有一個實例的創(chuàng)建是有必要的。這個String的構(gòu)造器的參數(shù)"bikini"他自己就是一個String實例,功能上和所有調(diào)用構(gòu)造器創(chuàng)建出來的實例是相同的。如果這種使用在一個循環(huán)的或者頻繁地調(diào)用,成千上萬的不必要的String實例就會被創(chuàng)建!
? ? 升級版是這樣:
String s = "bikini"
? ? 這個版本使用了一個String實例而不是每一次就去創(chuàng)建一個String實例,除此之外,這樣所會鼓勵對象這個對象在其他運行在相同虛擬機的String對象能夠被重用,因為虛擬機中能夠包含這個字符串字面量!
? ? 你可能經(jīng)常通過使用靜態(tài)工廠方法(條目1)而不是構(gòu)造器,同時僅向外提供私有構(gòu)造器,來創(chuàng)建不可變對象以避免創(chuàng)建不必要的對象。例如,使用Boolean.valueOf就比Boolean的構(gòu)造(在JAVA9中已經(jīng)被遺棄)更好。每一次使用構(gòu)造器一定會創(chuàng)建一個對象。然而靜態(tài)工廠方法在實際上從來不會這樣做。除此之外,重用不可變對象,你可以也重用可變對象當(dāng)你知道他不會變化!
? ? 有一些對象的創(chuàng)建會更加昂貴。如果你需要重用這樣一個昂貴的對象,他可能會因為重用被建議去做緩存!不幸的是,這不總是很明顯當(dāng)你創(chuàng)建一個對象。假設(shè)你想要寫一個方法來決定一個字符串是否是一個有效的羅馬數(shù)字,這時最簡單的方式就是使用正則表達式。
// 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})$");
}
? ? ? ? 這一個實現(xiàn)的問題在于它很依賴String.matches方法,然而String.matches是最簡單的方式來判斷一個字符串是否匹配一個正則表達式,如果重復(fù)使用在性能臨界區(qū)不合適,這個問題就會在本地為這個正則表達式創(chuàng)建一個正則表達式的模式實例,同時僅僅只會使用它一次。在它可以被垃圾回收器清理的時候,創(chuàng)建一個模式實例就會是很昂貴的了,因為模式需要編譯正則表達式到優(yōu)先的狀態(tài)機。
? ? 為了提高這個實現(xiàn)的性能,明確地去編譯這個正則表達式到一個模式實例作為類初始化的一部分,緩存這個模式,同時在每一次調(diào)用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();
? }
}
? ? 這個升級版提供了一個更好的性能如果它頻繁地被調(diào)用,在我的機器上,這個原始的版本使用了1.1 μs在一個8字符的輸入字符串,而升級版只使用了0.17?μs。不只是更快。而且可以證明,這樣所更加清晰,讓一個靜態(tài)filnal字段去存儲一個課件的模式實例允許我們給他一個命名,這樣有更好的可讀性在正則表達式上。
? ? 如果這個類包含這個isRomanNumeral 的升級版是已經(jīng)初始化過的,但是這個方法確實從來沒有調(diào)用過,那么這個模式的字段將是一個沒有必要去實例化的字段,我們有機會通過懶加載機制(條目83)來消除這個實例。他就編譯一個沒有可度量升級的實現(xiàn)。
? ? ? 當(dāng)一個對象是不可變的,它明顯可以被安全的重用,但是也可能有時候并不是那么明顯甚至違反我們的直覺。思考這樣一個適配器的問題,又被稱為視圖(views),一個適配器代表一個提供可以替代接口的的支持對象,因為一個適配器除了他的支持對象就沒有狀態(tài),這里不需要再創(chuàng)建超過一個對象來傳給適配器,因為他們需要的對象是相同的!
? ? 例如,Map接口的KeySet方法返回一個Map對象的Set視圖,由所有在Map中的。natively,這看起來每一次調(diào)用KeySet都會不得不創(chuàng)建一個Set實例,但是每一次在Map接口上調(diào)用KeySet可能返回相同的Set實例,盡管返回的實例是一個典型的不可變化的對象,所有但會的對象功能上都是相同的。當(dāng)一個返回的對象改變,所有其他的對象都會改變,因為他們都是來自于一個相同的Map實例。盡管通常在創(chuàng)建KeySet的時候多去創(chuàng)建一個實例是沒有害處的,但是這樣做其實也是沒有必要也沒有用處的。
? ? ? 另一個方式去創(chuàng)建不必要的對象是自動拆裝箱,它允許程序員包裝原始的類型成一個引用類型,以及一個對應(yīng)的引用類型自動拆成原始類型。在原始類型和包裝類型上有一些明顯的語法區(qū)別,同時也有一些不那么微妙的不同。思考一下這樣一個方法,它需要計算所有原始int的和,為了做這件事,程序不得不得使用一個long類型來存儲所有int的值因為int的大小不足以存儲所有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;
}
? ? ?這段程序可以得到一個正確的答案,但是它比它應(yīng)該做到的更慢,這就是一個由于一個Long的一個單字節(jié)L本該是小寫而寫成大寫造成的。這樣意味著程序?qū)⑦M行2的31次方的不必要的Long的實例的生成(粗略地計算每一次會for會產(chǎn)生一個實例,其實java會對一些小的值進行緩存)。在我的機器上,將這個變量的聲明從Long變?yōu)閘ong可以將這段程序的運行時間從6.3s降低到0.59秒!這個問題就很清楚了,優(yōu)先使用原始類型而不是包裝類型。同時也要注意到不小心的自動拆裝箱。
? ?這個條目不應(yīng)該被誤解為暗示了對象創(chuàng)建時昂貴的、而是應(yīng)該被避免!相反的是,一些小的對象的構(gòu)造器只做了一些很少的事,這些對象的創(chuàng)建時很廉價的,尤其是在現(xiàn)代的JVM的實現(xiàn)上,創(chuàng)建一個多余的對象讓程序更加清晰和簡單,通常是一件更好的事。
? 相反的是,保持一個對象池來避免多余的對象的創(chuàng)建通常是一個很壞的idea!除非這些在池中的對象的創(chuàng)建是一個花費非常高的過程。典型的需要對象池的例子就是數(shù)據(jù)庫連接。建立一個連接真的比確定來重新使用這個對象的代價更高。通常來說,無論如何,保持一個的對象池,會擾亂你的代碼,會提高內(nèi)存的占用,同時也會有一些不好的表現(xiàn)?,F(xiàn)代的JVM實現(xiàn)已經(jīng)有很好的優(yōu)化垃圾回收能力很容易處理一些輕量級的對象。
? 這個條目的中心點時條目50的防止復(fù)制?,F(xiàn)在的條目是說,“不要在你可以重用一個對象的時候創(chuàng)建一個新的對象?!庇涀。?dāng)不調(diào)用對象復(fù)制時,重復(fù)使用一個對象的懲罰是要遠遠好于創(chuàng)建一個不需要對象的的懲罰。如果防止對象出現(xiàn)失敗的話,就可能會導(dǎo)致一個潛在的bug和安全問題,創(chuàng)建一個不必要的對象僅僅只會影響代碼風(fēng)格和性能。