Java數值類型提升機制

Java語法特性隱藏在了代碼中的每個角落,最常見的就是自動拆裝箱和類型提升了。這些特性在帶來編碼便利性的同時也在代碼中藏下了些不易察覺定時炸彈,比如對null拆箱時引發的空指針異常NPE。本文就將JLS中關於數值提升的機制譯述出來,便於更深刻地理解代碼後面的東西。

問題

以下幾段代碼爲什麼是這樣的運行結果:

Object k = true ? null : 1;
System.out.println(k);

// 輸出:
null
Integer a = null;
Object k = true ? a : 1;
System.out.println(k);

// 運行時報NPE錯誤
byte a = 2;
byte k = true ? a : 128;
System.out.println(k);

// 編譯時報錯。 不兼容的類型: 從int轉換到byte可能會有損失

數值提升

數字類型提升機制被用於算術運算符上,通常使用場景爲:

  • 同一類型轉換
    雖然並無什麼作用,但有時可以使代碼更清晰。
  • 拓寬原始類型轉換
    指byte、short、int、long、float、double由低向高轉換。
  • 自動拆箱轉換
    基礎類型引用類的拆箱方法,如r.intValue()

數值提升用於將算術運算中的操作數轉化爲一個相同的類型以便於運算,具體分爲兩種情況:一元數值提升和二元數值提升。

一元數值提升

某些運算符將一元數值提升用在了單操作數運算中,其必定能得到一個數字類型的值,規則如下:

  • if 操作數是編譯時類型ByteShortCharacterInteger,那麼它會先拆箱爲對應的原始類型,然後拓寬爲int類型
  • else if 操作數爲編譯時類型LongFloatDouble,那麼就直接拆箱爲對應的原始類型。
  • else if 操作數是編譯時類型byteshortcharint,那麼就拓寬爲int類型
  • else 保持原樣。

一元數值提升還用在以下情境的表達式中(提升爲int):

  • 數組創建表達式的維度
  • 數組索引表達式的索引
  • 正號運算符(+)的操作數
  • 負號運算符(-)的操作數
  • 按位補運算符(~)的操作數
  • 移位運算符(>>, >>>, << )的每一個操作數。注意移位運算並不會使兩邊的操作數提升到相同類型,如 A << B 中若B爲long類型,A並不會被提升到long

注意:自增和自減單目運算符同樣也會進行類型提升,但運算後會自動進行強制類型轉換,如
byte a = 127;
a++; // a在運算後爲int類型,轉爲byte截斷後變成-128

等價於
byte a = (byte)128;

例:

class Test {
    public static void main(String[] args) {
        byte b = 2;
        int a[] = new int[b];  // 維度表達式提升
        char c = '\u0001';
        a[c] = 1;              // 索引表達式提升
        a[0] = -c;             // 負號 提升
        System.out.println("a: " + a[0] + "," + a[1]);
        b = -1;
        int i = ~b;            // 按位補提升
        System.out.println("~0x" + Integer.toHexString(b)
                           + "==0x" + Integer.toHexString(i));
        i = b << 4L;           // 移位提升(左操作數)
        System.out.println("0x" + Integer.toHexString(b)
                           + "<<4L==0x" + Integer.toHexString(i));
    }
}

輸出:
a: -1,1
~0xffffffff==0x0
0xffffffff<<4L==0xfffffff0

二元數值提升

當二元運算符的操作數皆可轉化爲數字類型時,那麼將採用如下二元數值提升規則:

  • 如果任一操作數爲引用類型,那麼對其進行自動拆箱。
  • 拓寬類型轉換被應用於以下情況:
    • if 某一操作數爲double類型,那麼另一個也轉爲double
    • else if 某一操作數爲float類型,那麼另一個也轉爲float
    • else if 某一操作數爲long類型,那麼另一個也轉爲long
    • else 兩個操作數都轉爲int

二元數值提升應用於以下運算符上:

  • 乘法運算符: * 、 / 、%
  • 針對數字類型的加減運算符: + 、 -
  • 數值比較運算符:< 、<= 、> 、>=
  • 數值相等比較運算符: == 、 !=
  • 整數按位運算符: & 、^ 、|
  • 某些情況下的條件運算符 ? : 中,後面將詳解

注意:混合賦值運算符同樣也會自動進行強制類型轉換,如
byte a = 127;
a += 1; // a在運算後爲int類型,轉爲byte截斷後變成-128

例:

class Test {
    public static void main(String[] args) {
        int i    = 0;
        float f  = 1.0f;
        double d = 2.0;
        // int*float 先是被提升爲 float*float,然後
        // float==double 被提升爲 double==double:
        if (i * f == d) System.out.println("oops");

        // char&byte 被提升爲 int&int:
        byte b = 0x1f;
        char c = 'G';
        int control = c & b;
        System.out.println(Integer.toHexString(control));

        // 此處 int:float 被提升爲 float:float:
        f = (b==0) ? i : 4.0f;
        System.out.println(1.0/f);
    }
}

輸出:
7
0.25

補充:條件運算符(? :)中的類型提升

三元運算符的數值提升機制較爲複雜,這兒詳細介紹分析一下。

條件運算符? : 介紹

條件運算符? : 在語法上是右結合的(從右到左結合)。因此, a?b:c?d:e?f:g 等價於 a?b:(c?d:(e?f:g))

條件運算符由三個表達式構成,第一個表達式的值類型必須爲 booleanBoolean,否則將產生編譯時錯誤。

當第二個或第三個表達式爲void方法時,也將拋出編譯時錯誤。

條件運算符的類型

條件表達式最終產生的類型取決於下述情況:

  • if 第二個操作數和第三個操作數有相同的類型(可以都爲null),那麼它就是條件表達式的類型。
  • else if 兩個操作數中有一個的類型爲原始類型T,而另一個爲T的裝箱類型,那麼條件表達式的類型就是T。
  • else if 其中一個操作數是編譯時null類型,另一個爲引用類型,那麼條件表達式的類型就是該引用類型。
  • else if 兩個操作數都可轉化爲數字類型,那麼分爲以下情況:
    • if 其中一個類型爲byteByte,另一個類型爲shortShort,那麼條件表達式類型爲short。
    • else if 其中一個類型爲T,T爲byteshortchar,另一個類型爲int類型的常量表達式,且可用T來表達(在T可表示的範圍內),那麼條件表達式類型爲T。
    • else if 其中一個類型爲T,T爲ByteShortCharacter,另一個類型爲int類型的常量表達式,且可用U來表達(U爲T的拆箱類型),那麼條件表達式類型爲U。
    • else 對兩個操作數使用二元數值提升機制(並沒有真的去轉換類型),得到的相同數值類型就是條件表達式的類型。

以上操作僅用於編譯器判斷條件表達式的最終類型T,只有在最終選擇的操作數(第二個表達式的或第三個表達式的)與T不符時纔會進行自動拆箱/類型提升操作

總結一下,運算中的類型提升通常都是將低於int位數的類型提升爲int,高於int的拆箱後保持不變,兩邊操作數位數不同則升爲高精度的那一個類型。

解題

最後我們分析下開頭的幾個問題:

Object k = true ? null : 1;
System.out.println(k);

// 輸出:
null

注意上面的null爲編譯時類型,1此時會自動裝箱爲Integer類型,此時按照上述規則條件表達式的類型爲Integer類型。因爲條件表達式的結果爲操作數null,所以k的實際類型爲Integer,值爲null

Integer a = null;
Object k = true ? a : 1;
System.out.println(k);

// 運行時報NPE錯誤

上面條件表達式中的a在編譯時被識別爲Integer類型,而非null類型,按照上述規則條件表達式的最終類型爲int類型。因爲條件表達式的結果操作數a與最終類型不符,所以此時將對a進行自動拆箱操作(a.intValue()),由於a運行時爲null,因此將報NPE錯誤。

byte a = 2;
byte k = true ? a : 128;
System.out.println(k);

// 編譯時報錯。 不兼容的類型: 從int轉換到byte可能會有損失

由於128超出了byte的範圍,因此返回值爲inta將轉化爲int返回,而接收方kbyte類型,向低精度轉化時需要顯示強制轉換才行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章