ITEM 6: 避免創(chuàng)建不必要的實(shí)例

ITEM 6: AVOID CREATING UNNECESSARY OBJECTS
??在每次需要時(shí)重用一個(gè)對象,而不是創(chuàng)建功能相同的新對象,是一種更合適的方案。重用效率更高。如果對象是不可變的,那么它總是可以重用的。
作為一個(gè)典型的反例,考慮下面的代碼:
String s = new String("bikini"); // DON'T DO THIS!
??上面這條語句每次會(huì)創(chuàng)建一個(gè)新的String實(shí)例,然而這是不必要的。String 構(gòu)造函數(shù)的參數(shù)(“bikini”)本身是一個(gè)String實(shí)例,在功能上與構(gòu)造函數(shù)創(chuàng)建的所有對象相同。如果這種用法被用在循環(huán)或經(jīng)常調(diào)用的方法中,則會(huì)不必要地創(chuàng)建數(shù)百萬個(gè)String實(shí)例。
改進(jìn)后的版本如下:
String s = "bikini";
??這個(gè)版本使用一個(gè)字符串實(shí)例,而不是每次執(zhí)行時(shí)創(chuàng)建一個(gè)新的字符串實(shí)例。此外,這樣做還能保證這個(gè)字符串對象將被運(yùn)行在同一虛擬機(jī)中的任何其他代碼重用,而該虛擬機(jī)恰好包含相同的字符串文本(常量池)。
??通過使用靜態(tài)工廠方法而不是構(gòu)造函數(shù),可以避免創(chuàng)建不必要的實(shí)例。例如 Boolean.valueOf(String) 要優(yōu)于 Boolean(String) (Java 9 中已廢棄)。構(gòu)造函數(shù)必須返回一個(gè)實(shí)例,而靜態(tài)工廠方法沒有被要求這么做,實(shí)踐中常常也不會(huì)這么做。不可變對象是天然可重用的,此外如果是可變對象,如果你確認(rèn)它們沒有被改變,那么也是可重用的。
??有些對象實(shí)例化的開銷非常高,如果我們需要頻繁地實(shí)例化,那么可以考慮緩存它們以便重用。不幸的是,當(dāng)我們寫代碼時(shí)并不是那么容易注意到這件事。假設(shè)我們想編寫一個(gè)方法來確定字符串是否是有效的羅馬數(shù)字,下面是使用正則表達(dá)式最簡單的方法:

// 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})$");
}

??上面這個(gè)方法的問題在于 String.matches,它是檢測字符串是否匹配正則表達(dá)式的最簡單的辦法,但它并不適合在關(guān)注性能的場景下重復(fù)使用。問題在于,它在內(nèi)部為正則表達(dá)式創(chuàng)建一個(gè) Pattern 實(shí)例,并且只使用它一次,之后就將這個(gè)實(shí)例拋棄了,它將做為不再使用的對象被垃圾回收。因?yàn)樾枰獙φ齽t表達(dá)式進(jìn)行編譯,Pattern 實(shí)例化的代價(jià)比較高。為了提高性能,顯式地編譯正則表達(dá)式,作為類初始化的一部分并緩存它,在每次調(diào)用 isRomanNumeral() 方法時(shí)重用相同的 Pattern 實(shí)例:

// 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();
  } 
}

??這個(gè)版本的 isRomanNumeral() 方法性能有很大提升。在我(作者)的機(jī)器上, 原始版本耗時(shí)1.1μs (8字節(jié)字符串),而改進(jìn)的版本耗時(shí)0.17μs,快了6.5倍。不僅性能得到了改善,而且清晰度也得到了提高。我們可以給這個(gè)不可變的 Pattern 成員對象命名,這比正則表達(dá)式本身可讀性強(qiáng)得多。
??如果 isRomanNumeral() 方法未被調(diào)用,那么 ROMAN 根本不需要初始化。我們可以通過延遲初始化來實(shí)現(xiàn)這一點(diǎn),不過不建議這樣做。與通常的延遲初始化一樣,這將使實(shí)現(xiàn)變得復(fù)雜,并且沒有可度量的性能改進(jìn)。
??當(dāng)一個(gè)實(shí)例是不可變得,那么毫無疑問它是能夠安全重用的,但有些情況就不那么顯而易見,甚至是違反直覺的。考慮一個(gè)adapters(適配器)的例子,適配器是這樣一個(gè)對象,它代表一個(gè)委托的對象,對外提供一個(gè)可替代的接口。因?yàn)檫m配器是無狀態(tài)的,所以不需要為給定對象創(chuàng)建多個(gè)給定適配器實(shí)例。例如,keySet() 方法為 Map 接口提供一個(gè) Map 中所有 key 的集合視圖。自然的,每次調(diào)用 keySet() 方法都應(yīng)該返回一個(gè)新的 Set 實(shí)例,但對于一個(gè)指定的Map,每次調(diào)用 keySet() 都有可能返回相同的 Set 實(shí)例。雖然 Set 實(shí)例通常是可變的,但是在這個(gè)例子中所有返回的Set 實(shí)例在功能上是相同的: 當(dāng)一個(gè)返回的對象發(fā)生更改時(shí),所有其他對象也會(huì)發(fā)生更改,因?yàn)樗鼈兌加上嗤腗ap實(shí)例支持。雖然創(chuàng)建 keySet 視圖對象的多個(gè)實(shí)例在很大程度上是無害的,但這是不必要的,也沒有好處。
??另一種創(chuàng)建不必要實(shí)例的場景時(shí)裝箱/拆箱。自動(dòng)裝箱模糊了原始類型和裝箱原始類型之間的區(qū)別,但并沒有消除它們。它們有細(xì)微的語義差別和不那么細(xì)微的性能差別??紤]下面的方法,它計(jì)算所有正整數(shù)值的和。要做到這一點(diǎn),程序必須使用long類型,因?yàn)閕nt不夠大,不能容納所有正整數(shù)值的和:

// 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; 
}

??這個(gè)程序能得到正確答案,但性能比預(yù)期的差遠(yuǎn)了。變量 sum 被定義為 Long 而不是 long,這意味著程序毫無必要的創(chuàng)建了 2^31 個(gè) Long 實(shí)例(在每次 sum += i 時(shí)),在我(作者)的機(jī)器上,將 sum 的聲明從 Long 更改為 long 可以將運(yùn)行時(shí)從6.3秒減少到0.59秒。教訓(xùn)很明顯的: 優(yōu)先使用原語而不是包裝類型,并且要注意無意的自動(dòng)裝箱。
??上面這個(gè)例子并不是在暗示創(chuàng)建對象是非常昂貴的,應(yīng)該避免創(chuàng)建對象。恰恰相反,創(chuàng)建和回收小對象的成本很低,特別是在現(xiàn)代JVM上。創(chuàng)建額外的對象來增強(qiáng)程序的清晰度、簡潔性或功能通常是一件好事。
??相反,通過維護(hù)自己的對象池來避免對象創(chuàng)建是一個(gè)壞主意,除非池中的對象非常重量級。一個(gè)使用對象池的經(jīng)典例子是數(shù)據(jù)庫連接,建立連接的成本非常高,因此有必要重用這些對象。但是,一般來說,維護(hù)自己的對象池會(huì)使代碼混亂,增加內(nèi)存占用,并損害性能?,F(xiàn)代JVM實(shí)現(xiàn)具有高度優(yōu)化的垃圾收集器,這些收集器在輕量級對象上很容易勝過此類對象池。
??這個(gè)條目的對應(yīng)點(diǎn)是針對條目 50的防御性復(fù)制(defensive copying)。 當(dāng)前項(xiàng)說,“當(dāng)應(yīng)該重用現(xiàn)有對象時(shí),不要?jiǎng)?chuàng)建新對象”,而第50項(xiàng)說,“當(dāng)應(yīng)該創(chuàng)建新對象時(shí),不要重用現(xiàn)有對象”。請注意,在需要防御性復(fù)制時(shí)重用對象的代價(jià)遠(yuǎn)遠(yuǎn)大于不必要地創(chuàng)建重復(fù)對象的代價(jià)。未能在需要的地方復(fù)制防御副本,可能會(huì)導(dǎo)致潛在的bug和安全漏洞;創(chuàng)建不必要的對象只會(huì)影響樣式和性能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容