Effective Java 第三版——6. 避免建立沒必要要的對象

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。程序員

Effective Java, Third Edition

6. 避免建立沒必要要的對象

在每次須要時重用一個對象而不是建立一個新的相同功能對象一般是恰當的。重用能夠更快更流行。若是對象是不可變的(條目 17),它老是能夠被重用。正則表達式

做爲一個不該該這樣作的極端例子,請考慮如下語句:數據庫

String s = new String("bikini");  // DON'T DO THIS!

語句每次執行時都會建立一個新的String實例,而這些對象的建立都不是必需的。String構造方法(「bikini」)的參數自己就是一個bikini實例,它與構造方法建立的全部對象的功能相同。若是這種用法發生在循環中,或者在頻繁調用的方法中,就能夠毫無必要地建立數百萬個String實例。緩存

改進後的版本以下:安全

String s = "bikini";

該版本使用單個String實例,而不是每次執行時建立一個新實例。此外,它能夠保證對象運行在同一虛擬機上的任何其餘代碼重用,而這些代碼剛好包含相同的字符串字面量[JLS,3.10.5]。ide

經過使用靜態工廠方法(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();
    }
}

若是常常調用,isRomanNumeral的改進版本的性能會顯著提高。 在個人機器上,原始版本在輸入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說:「不要重複使用現有的對象,當你應該建立一個新的對象時。」請注意,重用防護性複製所要求的對象所付出的代價,要遠遠大於沒必要要地建立重複的對象。 未能在須要的狀況下防護性複製會致使潛在的錯誤和安全漏洞;而沒必要要地建立對象只會影響程序的風格和性能。

相關文章
相關標籤/搜索