天天看點

Effective Java 第三版——61. 基本類型優于裝箱的基本類型

Tips

書中的源代碼位址:https://github.com/jbloch/effective-java-3e-source-code

注意,書中的有些代碼裡方法是基于Java 9 API中的,是以JDK 最好下載下傳 JDK 9以上的版本。

61. 基本類型優于裝箱的基本類型

Java是一個由兩部分類型組成的系統,一部分由基本類型組成,如int,double和boolean,還有一部分是引用類型,如String和List。 每個基本類型都有一個相應的引用類型,稱為裝箱基本類型。 對應于int,double和boolean的包裝基本類型是Integer,Double和Boolean。

正如條目6中提到的,自動裝箱和自動拆箱模糊了基本類型和裝箱基本類型之間的差別,但不會消除它們。這兩者之間有真正的差別,重要的是要始終意識到你正在使用的是哪一種,并在它們之間仔細選擇。

基本類型和包裝基本類型之間有三個主要差別。首先,基本類型隻有它們的值,而包裝基本類型具有與其值不同的辨別。換句話說,兩個包裝基本類型執行個體可以具有相同的值但不同的引用辨別。第二,基本類型隻有功能的值(functional value),而每個包裝基本類型類型除了對應的基本類型的功能值外,還有一個非功能值,即null。最後,基本類型比包裝的基本類型更節省時間和空間。如果你不小心的話,這三種差異都會給你帶來真正的麻煩。

考慮下面的比較器,它的設計目的是表示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值大于第二個值!

那麼問題出在哪裡呢?

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就可以了。

最後,考慮第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。最後,當程式裝箱了基本類型,可能會導緻代價高昂且建立了不必要的對象。