謎題24:盡情享受每一個字節
下面的程序循環遍歷byte數值,以查找某個特定值。這個程序會打印出什麽呢?
public class BigDelight {
public static void main(String[] args) {
for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) {
if (b == 0x90)
System.out.print("Joy!");
}
}
}
這個循環在除了Byte.MAX_VALUE之外所有的byte數值中進行叠代,以查找0x90。這個數值適合用byte表示,並且不等於Byte.MAX_VALUE,因此你可能會想這個循環在該叠代會找到它一次,並將打印出Joy!。但是,所見為虛。如果你運行該程序,就會發現它沒有打印任何東西。怎麽回事?
簡單地說,0x90是一個int常量,它超出了byte數值的範圍。這與直覺是相悖的,因為0x90是一個兩位的十六進制字面常量,每一個十六進制位都占據4個比特的位置,所以整個數值也只占據8個比特,即1個byte。問題在於byte是有符號類型。常量0x90是一個正的最高位被置位的8位int數值。合法的byte數值是從-128到+127,但是int常量0x90等於+144。
拿一個byte與一個int進行的比較是一個混合類型比較(mixed-type comparison)。如果你把byte數值想象為蘋果,把int數值想象成為桔子,那麽該程序就是在拿蘋果與桔子比較。請考慮表達式((byte)0x90 == 0x90),盡管外表看起來是成立的,但是它卻等於false。
為了比較byte數值(byte)0x90和int數值0x90,Java通過拓寬原始類型轉換將byte提升為一個int[JLS 5.1.2],然後比較這兩個int數值。因為byte是一個有符號類型,所以這個轉換執行的是符號擴展,將負的byte數值提升為了在數字上相等的int數值。在本例中,該轉換將(byte)0x90提升為int數值-112,它不等於int數值0x90,即+144。
由於系統總是強制地將一個操作數提升到與另一個操作數相匹配的類型,所以混合類型比較總是容易把人搞糊塗。這種轉換是不可視的,而且可能不會產生你所期望的結果。有若幹種方法可以避免混合類型比較。我們繼續有關水果的比喻,你可以選擇拿蘋果與蘋果比較,或者是拿桔子與桔子比較。你可以將int轉型為byte,之後你就可以拿一個byte與另一個byte進行比較了:
if (b == (byte)0x90)
System.out.println("Joy!");
或者,你可以用一個屏蔽碼來消除符號擴展的影響,從而將byte轉型為int,之後你就可以拿一個int與另一個int進行比較了:
if ((b & 0xff) == 0x90)
System.out.print("Joy!");
上面的兩個解決方案都可以正常運行,但是避免這類問題的最佳方法還是將常量值移出到循環的外面,並將其在一個常量聲明中定義它。下面是我們對此作出的第一個嘗試:
public class BigDelight {
private static final byte TARGET = 0x90;
public static void main(String[] args) {
for (byte b = Byte.MIN_VALUE; b <
Byte.MAX_VALUE; b++) {
if (b == TARGET)
System.out.print("Joy!");
}
}
}
遺憾的是,它根本就通不過編譯。常量聲明有問題,編譯器會告訴你問題所在:0x90對於byte類型來說不是一個有效的數值。如果你想下面這樣訂正該聲明,那麽程序將運行得非常好:
private static final byte TARGET = (byte)0x90;
總之,要避免混合類型比較,因為它們內在地容易引起混亂(謎題5)。為了幫助實現這個目標,請使用聲明的常量替代“魔幻數字”。你已經了解了這確實是一個好主意:它說明了常量的含義,集中了常量的定義,並且根除了重復的定義。現在你知道它還可以強制你去為每一個常量賦予適合其用途的類型,從而消除了產生混合類型比較的一種根源。
對語言設計的教訓是byte數值的符號擴展是產生bug和混亂的一種常見根源。而用來抵銷符號擴展效果所需的屏蔽機制會使得程序顯得混亂無序,從而降低了程序的可讀性。因此,byte類型應該是無符號的。還可以考慮為所有的原始類型提供定義字面常量的機制,這可以減少對易於產生錯誤的類型轉換的需求(謎題27)。
謎題24:盡情享受每一個字節