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 操作數是編譯時類型
Byte
、Short
、Character
或Integer
,那麼它會先拆箱爲對應的原始類型,然後拓寬爲int
類型。 - else if 操作數爲編譯時類型
Long
、Float
或Double
,那麼就直接拆箱爲對應的原始類型。 - else if 操作數是編譯時類型
byte
、short
、char
或int
,那麼就拓寬爲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
- if 某一操作數爲
二元數值提升應用於以下運算符上:
- 乘法運算符: * 、 / 、%
- 針對數字類型的加減運算符: + 、 -
- 數值比較運算符:< 、<= 、> 、>=
- 數值相等比較運算符: == 、 !=
- 整數按位運算符: & 、^ 、|
- 某些情況下的條件運算符 ? : 中,後面將詳解
注意:混合賦值運算符同樣也會自動進行強制類型轉換,如
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))
。
條件運算符由三個表達式構成,第一個表達式的值類型必須爲 boolean
或 Boolean
,否則將產生編譯時錯誤。
當第二個或第三個表達式爲void
方法時,也將拋出編譯時錯誤。
條件運算符的類型
條件表達式最終產生的類型取決於下述情況:
- if 第二個操作數和第三個操作數有相同的類型(可以都爲
null
),那麼它就是條件表達式的類型。 - else if 兩個操作數中有一個的類型爲原始類型T,而另一個爲T的裝箱類型,那麼條件表達式的類型就是T。
- else if 其中一個操作數是編譯時
null
類型,另一個爲引用類型,那麼條件表達式的類型就是該引用類型。 - else if 兩個操作數都可轉化爲數字類型,那麼分爲以下情況:
- if 其中一個類型爲
byte
或Byte
,另一個類型爲short
或Short
,那麼條件表達式類型爲short。 - else if 其中一個類型爲T,T爲
byte
、short
或char
,另一個類型爲int
類型的常量表達式,且可用T來表達(在T可表示的範圍內),那麼條件表達式類型爲T。 - else if 其中一個類型爲T,T爲
Byte
、Short
或Character
,另一個類型爲int
類型的常量表達式,且可用U來表達(U爲T的拆箱類型),那麼條件表達式類型爲U。 - else 對兩個操作數使用二元數值提升機制(並沒有真的去轉換類型),得到的相同數值類型就是條件表達式的類型。
- if 其中一個類型爲
以上操作僅用於編譯器判斷條件表達式的最終類型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
的範圍,因此返回值爲int
,a
將轉化爲int
返回,而接收方k
爲byte
類型,向低精度轉化時需要顯示強制轉換才行。