一般來(lái)說(shuō),最好是重用對(duì)象而不是在每次需要的時(shí)候就創(chuàng)建一個(gè)相同功能的新對(duì)象。重用方式既更加快速,也更為流行。如果對(duì)象是不可變的(immutable)(見(jiàn)第15條),它就始終可以被重用。
作為一個(gè)極端的反面例子,考慮下面的語(yǔ)句:
String s = new String("hello world"); // DON'T DO THIS!
該語(yǔ)句每次被執(zhí)行的時(shí)候都創(chuàng)建一個(gè)新的String實(shí)例,但是這些創(chuàng)建對(duì)象的動(dòng)作全都是不必要的。傳遞給String構(gòu)造器的參數(shù)("hello world")本身就是一個(gè)String實(shí)例,功能方面等同于構(gòu)造器創(chuàng)建的所有對(duì)象。如果這種用法是在一個(gè)循環(huán)中,或者是在一個(gè)被頻繁調(diào)用的方法中,就會(huì)創(chuàng)建出成千上萬(wàn)不必要的String實(shí)例。
改進(jìn)后的版本如下所示:
String s = "hello world";
這個(gè)版本只用了一個(gè)String實(shí)例,而不是每次執(zhí)行的時(shí)候都創(chuàng)建一個(gè)新的實(shí)例。而且,它可以保證,對(duì)于所有在同一臺(tái)虛擬機(jī)中運(yùn)行的代碼,只要它們包含相同的字符串字面常量,該對(duì)象就會(huì)被重用[JLS, 3.10.5]。
對(duì)于同時(shí)提供了靜態(tài)工廠方法(見(jiàn)第1條)和構(gòu)造器的不可變類(lèi),通??梢允褂渺o態(tài)工廠方法而不是構(gòu)造器,以避免創(chuàng)建不必要的對(duì)象。例如,靜態(tài)工廠方法Boolean.valueOf (String)幾乎總是優(yōu)先于構(gòu)造器Boolean(String)。構(gòu)造器在每次被調(diào)用的時(shí)候都會(huì)創(chuàng)建一個(gè)新的對(duì)象,而靜態(tài)工廠方法則從來(lái)不要求這樣做,實(shí)際上也不會(huì)這樣做。
除了重用不可變的對(duì)象之外,也可以重用那些已知不會(huì)被修改的可變對(duì)象。下面是一個(gè)比較微妙、也比較常見(jiàn)的反例,其中涉及到可變的Date對(duì)象,它們的值一旦計(jì)算出來(lái)之后就不會(huì)再有變化。這個(gè)類(lèi)建立了一個(gè)模型:其中有一個(gè)人,并有一個(gè)isBabyBoomer方法,用來(lái)檢驗(yàn)這個(gè)人是否為一個(gè)"baby boomer(生育高峰期出生的小孩)",換句話說(shuō),就是檢驗(yàn)這個(gè)人是否出生于1946至1964年間:

isBabyBoomer每次被調(diào)用的時(shí)候,都會(huì)新建一個(gè)Calendar、一個(gè)TimeZone和兩個(gè)Date實(shí)例,這是沒(méi)有必要的。下面的版本使用了一個(gè)靜態(tài)的初始化器(initializer),避免了這種效率低下的情況:

改進(jìn)后的Person類(lèi)只在初始化的時(shí)候創(chuàng)建Calendar、TimeZone和Date實(shí)例一次,而不是在每次isBabyBoomer被調(diào)用的時(shí)候都創(chuàng)建這些實(shí)例。如果isBabyBoomer方法得到頻繁的調(diào)用,這種方法將會(huì)顯著地提高性能。在我的機(jī)器上,每調(diào)用一千萬(wàn)次,原來(lái)的版本需要32,000ms,而改進(jìn)后的版本只需130ms,大約快了250倍。除了提高性能之外,代碼的含義也更加清晰了。把boomStart和boomEnd從局部變量改為final靜態(tài)域,這些日期顯然是被作為常量對(duì)待,從而使得代碼更易于理解。但是,這種優(yōu)化帶來(lái)的效果并不總是那么明顯,因?yàn)镃alendar實(shí)例的創(chuàng)建代價(jià)特別昂貴。
如果改進(jìn)后的Person類(lèi)被初始化了,它的isBabyBoomer方法卻永遠(yuǎn)不會(huì)被調(diào)用,那就沒(méi)有必要初始化BOOM_START和BOOM_END域。通過(guò)在isBabyBoomer方法第一次被調(diào)用的時(shí)候延遲初始化(lazily initializing)(見(jiàn)第71條)這些域,有可能消除這些不必要的初始化工作,但是不建議這樣做。正如延遲初始化(lazy initialization)中常見(jiàn)的情況一樣,這樣做會(huì)使方法的實(shí)現(xiàn)更加復(fù)雜,從而無(wú)法將性能顯著提高到超過(guò)已經(jīng)達(dá)到的水平(見(jiàn)第55條)。
在本條目前面的例子中,所討論到的對(duì)象顯然都是能夠被重用的,因?yàn)樗鼈儽怀跏蓟蟛粫?huì)再改變。其他有些情形則并不總是這么明顯了??紤]適配器(adapter)的情形[Gamma95, p. 139],有時(shí)也稱(chēng)作視圖(view)。適配器是指這樣一個(gè)對(duì)象:它把功能委托給一個(gè)后備對(duì)象(backing object),從而為后備對(duì)象提供一個(gè)可以替代的接口。由于適配器除了后備對(duì)象之外,沒(méi)有其他的狀態(tài)信息,所以針對(duì)某個(gè)給定對(duì)象的特定適配器而言,它不需要?jiǎng)?chuàng)建多個(gè)適配器實(shí)例。
例如,Map接口的keySet方法返回該Map對(duì)象的Set視圖,其中包含該Map中所有的鍵(key)。粗看起來(lái),好像每次調(diào)用keySet都應(yīng)該創(chuàng)建一個(gè)新的Set實(shí)例,但是,對(duì)于一個(gè)給定的Map對(duì)象,每次調(diào)用keySet都返回同樣的Set實(shí)例。雖然被返回的Set實(shí)例一般是可改變的,但是所有返回的對(duì)象在功能上是等同的:當(dāng)其中一個(gè)返回對(duì)象發(fā)生變化的時(shí)候,所有其他的返回對(duì)象也要發(fā)生變化,因?yàn)樗鼈兪怯赏粋€(gè)Map實(shí)例支撐的。雖然創(chuàng)建keySet視圖對(duì)象的多個(gè)實(shí)例并無(wú)害處,卻也是沒(méi)有必要的。
在Java 1.5發(fā)行版本中,有一種創(chuàng)建多余對(duì)象的新方法,稱(chēng)作自動(dòng)裝箱(autoboxing),它允許程序員將基本類(lèi)型和裝箱基本類(lèi)型(Boxed Primitive Type)混用,按需要自動(dòng)裝箱和拆箱。自動(dòng)裝箱使得基本類(lèi)型和裝箱基本類(lèi)型之間的差別變得模糊起來(lái),但是并沒(méi)有完全消除。它們?cè)谡Z(yǔ)義上還有著微妙的差別,在性能上也有著比較明顯的差別(見(jiàn)第49條)??紤]下面的程序,它計(jì)算所有int正值的總和。為此,程序必須使用long算法,因?yàn)閕nt不夠大,無(wú)法容納所有int正值的總和:

這段程序算出的答案是正確的,但是比實(shí)際情況要更慢一些,只因?yàn)榇蝈e(cuò)了一個(gè)字符。變量sum被聲明成Long而不是long,意味著程序構(gòu)造了大約2的31個(gè)多余的Long實(shí)例(大約每次往Long sum中增加long時(shí)構(gòu)造一個(gè)實(shí)例)。將sum的聲明從Long改成long,在我的機(jī)器上使運(yùn)行時(shí)間從43秒減少到了6.8秒。結(jié)論很明顯:要優(yōu)先使用基本類(lèi)型而不是裝箱基本類(lèi)型,要當(dāng)心無(wú)意識(shí)的自動(dòng)裝箱。
不要錯(cuò)誤地認(rèn)為本條目所介紹的內(nèi)容暗示著"創(chuàng)建對(duì)象的代價(jià)非常昂貴,我們應(yīng)該要盡可能地避免創(chuàng)建對(duì)象"。相反,由于小對(duì)象的構(gòu)造器只做很少量的顯式工作,所以,小對(duì)象的創(chuàng)建和回收動(dòng)作是非常廉價(jià)的,特別是在現(xiàn)代的JVM實(shí)現(xiàn)上更是如此。通過(guò)創(chuàng)建附加的對(duì)象,提升程序的清晰性、簡(jiǎn)潔性和功能性,這通常是件好事。
反之,通過(guò)維護(hù)自己的對(duì)象池(object pool)來(lái)避免創(chuàng)建對(duì)象并不是一種好的做法,除非池中的對(duì)象是非常重量級(jí)的。真正正確使用對(duì)象池的典型對(duì)象示例就是數(shù)據(jù)庫(kù)連接池。建立數(shù)據(jù)庫(kù)連接的代價(jià)是非常昂貴的,因此重用這些對(duì)象非常有意義。而且,數(shù)據(jù)庫(kù)的許可可能限制你只能使用一定數(shù)量的連接。但是,一般而言,維護(hù)自己的對(duì)象池必定會(huì)把代碼弄得很亂,同時(shí)增加內(nèi)存占用(footprint),并且還會(huì)損害性能。現(xiàn)代的JVM實(shí)現(xiàn)具有高度優(yōu)化的垃圾回收器,其性能很容易就會(huì)超過(guò)輕量級(jí)對(duì)象池的性能。
與本條目對(duì)應(yīng)的是第39條中有關(guān)"保護(hù)性拷貝(defensive copying)"的內(nèi)容。本條目提及"當(dāng)你應(yīng)該重用現(xiàn)有對(duì)象的時(shí)候,請(qǐng)不要?jiǎng)?chuàng)建新的對(duì)象",而第39條則說(shuō)"當(dāng)你應(yīng)該創(chuàng)建新對(duì)象的時(shí)候,請(qǐng)不要重用現(xiàn)有的對(duì)象"。注意,在提倡使用保護(hù)性拷貝的時(shí)候,因重用對(duì)象而付出的代價(jià)要遠(yuǎn)遠(yuǎn)大于因創(chuàng)建重復(fù)對(duì)象而付出的代價(jià)。必要時(shí)如果沒(méi)能實(shí)施保護(hù)性拷貝,將會(huì)導(dǎo)致潛在的錯(cuò)誤和安全漏洞;而不必要地創(chuàng)建對(duì)象則只會(huì)影響程序的風(fēng)格和性能。