Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
Java是一個由兩部分類型組成的系統,一部分由基本類型組成,如int,double和boolean,還有一部分是引用類型,如String和List。 每一個基本類型都有一個相應的引用類型,稱爲裝箱基本類型。 對應於int,double和boolean的包裝基本類型是Integer,Double和Boolean。git
正如條目6中提到的,自動裝箱和自動拆箱模糊了基本類型和裝箱基本類型之間的區別,但不會消除它們。這二者之間有真正的區別,重要的是要始終意識到你正在使用的是哪種,並在它們之間仔細選擇。程序員
基本類型和包裝基本類型之間有三個主要區別。首先,基本類型只有它們的值,而包裝基本類型具備與其值不一樣的標識。換句話說,兩個包裝基本類型實例能夠具備相同的值但不一樣的引用標識。第二,基本類型只有功能的值(functional value),而每一個包裝基本類型類型除了對應的基本類型的功能值外,還有一個非功能值,即null。最後,基本類型比包裝的基本類型更節省時間和空間。若是你不當心的話,這三種差別都會給你帶來真正的麻煩。github
考慮下面的比較器,它的設計目的是表示Integer值的升序數字順序。(回想一下,比較器的compare方法返回一個負數、零或正數,這取決於它的第一個參數是小於、等於仍是大於第二個參數)。你不須要在實踐中編寫這個比較器,由於它實現了Integer的天然排序,但它提供了一個有趣的例子:小程序
// Broken comparator - can you spot the flaw? Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
這個比較器看起來應該工做,也能經過不少測試。 例如,它能夠與Collections.sort
方法一塊兒使用,以正確排序百萬個元素列表,不管列表是否包含重複元素。 但這個比較器存在嚴重缺陷。 爲了說服本身,只需打印naturalOrder.compare(new Integer(42),new Integer(42))
的值。 兩個Integer實例都表示相同的值(42),所以該表達式的值應爲0,但它爲1,表示第一個Integer值大於第二個值!ide
那麼問題出在哪裏呢?naturalOrder
中的第一個測試工做得很好。計算表達式i < j
會使i和j引用的整數實例自動拆箱;也就是說,它提取它們的基本類型值。計算的目的是檢查獲得的第一個int值是否小於第二個int值。但假設是否認的。而後,下一個測試計算表達式i==j
,該表達式對兩個對象執行引用標識比較。若是i和j引用表示相同整型值的不一樣Integer實例,這個比較將返回false,比較器將錯誤地返回1,代表第一個整型值大於第二個整型值。將==操做符應用於裝箱的基本類型幾乎老是錯誤的。性能
在實踐中,若是你須要一個比較器來描述類型的天然順序,應該簡單地調用comparator . naturalorder()
方法,若是本身編寫一個比較器,應該使用比較器構造方法,或者對基本類型使用靜態compare方法(條目 14)。也就是說,能夠經過添加兩個局部變量來存儲與裝箱Integer參數對應的原始int值,並對這些變量執行全部的比較,從而修復了損壞的比較器中的問題。這樣避免了錯誤的引用一致性比較:測試
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> { int i = iBoxed, j = jBoxed; // Auto-unboxing return i < j ? -1 : (i == j ? 0 : 1); };
接下來,考慮一下這個有趣的小程序:設計
public class Unbelievable { static Integer i; public static void main(String[] args) { if (i == 42) System.out.println("Unbelievable"); } }
它不會打印出Unbelievable
字符串——但它所作的事情幾乎一樣奇怪。它在計算表達式i==42時
拋出NullPointerException
。問題是,i是Integer類型,而不是int類型,並且像全部很是量對象引用屬性同樣,它的初始值爲null。當程序計算表達式i==42
時,它是在比較Integer和int之間的關係。 幾乎在每種狀況下,當在基本類型和包裝基本類型進行混合操做時,包裝基本類型會自動拆箱。若是對一個null對象進行自動拆箱,那麼會拋出NullPointerException。正如這個程序所演示的,它幾乎能夠在任何地方發生。修復這個問題很是簡單,只需將i聲明爲int而不是Integer就能夠了。3d
最後,考慮第24頁條目6中的程序:
// Hideously slow program! Can you spot the object creation? public static void main(String[] args) { Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } System.out.println(sum); }
這個程序比它本來的速度慢得多,由於它意外地聲明瞭一個局部變量(sum),它是裝箱的基本類型Long,而不是基本類型long。程序在沒有錯誤或警告的狀況下編譯,變量被反覆裝箱和拆箱,致使觀察到的性能降低。
在本條目中討論的全部三個程序中,問題都是同樣的:程序員忽略了基本類型和包裝基本類型之間的區別,並承擔了後果。在前兩個項目中,結果是完全的失敗;第三,嚴重的性能問題。
那麼,何時應該使用裝箱基本類型呢?它們有幾個合法的用途。第一個是做爲集合中的元素、鍵和值。不能將基本類型放在集合中,所以必須使用裝箱的基本類型。這是通常狀況下的特例。在參數化類型和方法(第5章)中,必須使用裝箱基本類型做爲類型參數,由於該語言不容許使用基本類型。例如,不能將變量聲明爲ThreadLocal<int>
類型,所以必須使用ThreadLocal<Integer>
。最後,在進行反射方法調用時,必須使用裝箱基本類型(條目 65)。
總之,只要有選擇,就應該優先使用基本類型,而不是裝箱基本類型。基本類型更簡單、更快。若是必須使用裝箱基本類型,則須要當心!自動裝箱減小了使用裝箱基本類型的冗長,但沒有下降使用的危險。當程序使用==操做符比較兩個裝箱的基本類型時,它會執行引用標識比較,這幾乎確定不是你想要的。當程序執行包含裝箱和拆箱基本類型的混合類型計算時,它會執行拆箱,當程序執行拆箱時,會拋出NullPointerException。最後,當程序裝箱了基本類型,可能會致使代價高昂且建立了沒必要要的對象。