《java解惑》讀書筆記1——表達式之謎

《java解惑》是Google公司的首席Java架構師Joshua Bloch繼《Effective java》之後有一力作,專門揭示了很多java編程中意想不到的疑惑,很多有多年工作經驗的java開發人員在看完本書之後甚至都懷疑自己會不會寫java程序,本系列博客主要記錄在讀《java解惑》中的經典例子以及原因分析。

1.奇偶性判斷:

問題:

如果使用下面的程序判斷整數奇偶性會有什麼問題:

public static boolean isOdd(int i){
        return i % 2 == 1;
}
上述代碼對於正整數沒有任何問題,但是對於所有負奇數的判斷全部都是錯誤的。

原因:

java對於取餘運算符(%)的定義爲:對於所有int數值a和所有非零int數值b,都滿足如下恆等式:

(a / b) * b + (a % b) == a

當取餘操作返回一個非零的結果時,它與左操作數具有相同的正負號,因此負整數模2的餘數總數-1而非1.

解決辦法:

 (1).一般最常用的解決方法爲:

public static boolean isOdd(int i){
        return i % 2 == 0;
}
(2).對於性能要求的系統,可以使用如下的解決方法:

public static boolean isOdd(int i){
        return (i & 1) != 0;
}

2.浮點數精確運算:

浮點數運算時結果是近似值,如果需要使用精確值如金融計算方面,考慮使用BigDecimal。

注意:new BigDecimal(.9)其實還是浮點數,精確數使用new BigDecimal(".9")。


3.長整除運算:

問題:

下面的程序計算一天的微秒數除以毫秒數:

public static void main(String[] args) {
		final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
		final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
		System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
	}
我們期望輸出的結果是1000,但是運行的真正結果是5.

原因:

除數和被除數都是long型的,不會產生溢出的問題,但是由於除數和被除數都是以int類型計算出來的,MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000計算過程中中間結果仍然以int類型來表示,只在最後的計算結果才被提升擴展爲long類型,而int類型存放MICROS_PER_DAY在類型轉換之前會溢出。

解決辦法:

明白原因之後,解決辦法就很簡單,只需要在計算時明確顯式地指定計算因子爲long類型,代碼如下:

public static void main(String[] args) {
		final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
		final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
		System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
	}
這個問題的教訓是如果在計算比較大的數值時,計算因子最好限制轉換爲擴展後的類型以防止溢出。


4.十六進制混合運算:

問題:

下面一段程序演示兩個十六進制數運算:

public static void main(String[] args) {
		System.out.println(Long.toHexString(0x100000000L + 0xCAFEBABE));
	}
期望結果爲:1CAFEBABE,但是運行的真正結果爲:CAFEBABE,第33未的1丟失了。

原因:

java中十進制字面常量都是正的,對於負數必須要顯示添加負號(-),但是對於二進制,八進制和十六進制來說字面常量的正負數由最高位的符號位表示,如果最高位是1就表示負數,最高位爲0就表示整數。

對於long類型來說,能表示的最大十六進制數爲:FFFFFFFFFFFFFFFF,0x100000000L補全位數之後爲:0000000100000000,最高位爲0,表示爲正數。

對於int類型來說,能表示的最大十六進制數爲:FFFFFFFF,0xcafebabe補全位數之後仍然是CAFEBABE,十六進制和二進制轉換是1爲十六進制數對應4爲二進制數,C對應爲1100,因此0xCAFEBABE的最高位爲1,其實是負數。

在上述運算中,左操作數是long類型,有操作數是int類型,在計算時java會自動對int類型的數進行類型提升擴展爲long類型,由於最高位爲1,因此在類型提升時需要保持符號位,提升之後0xCAFEBABE變爲0xFFFFFFFFCAFEBABE,運算過程如下:

    0x0000000100000000

 +0xFFFFFFFFCAFEBABE

-----------------------------------------

= 0x00000000CAFEBABE

解決辦法:

該問題的根本原因在於int類型想long類型提升擴展時符號位的保持擴展造成的,解決辦法很簡單,只需要將int類型的0xCAFEBABE變爲long類型,避免符號位擴充即可,代碼如下:

public static void main(String[] args) {
		System.out.println(Long.toHexString(0x100000000L + 0xCAFEBABEL));
	}

 5.多重類型轉換:

問題:

下面代碼輸出結果是什麼:

public static void main(String[] args) {
		System.out.println((int)(char)(byte)-1);
	}
很多人認爲輸出的結果應該是-1,但是真正運行輸出的結果是65535。

原因:

java使用了二進制補碼運算,數據類型轉換依賴與符號位擴展:

int類型的-1的所有32爲都是置位的(高位和符號位全部置位爲1),當從32爲的int轉型到8爲的byte時,執行一個窄化原始類型轉換,直接保留低8位,得到的結果是8個全是1的二進制數,表示的仍舊是-1。

byte,int,long,short都是有符號類型,而char是一個無符號類型,在將一個8位的有符號byte轉換爲一個16位的無符號char類型時,首先將8位全是1的byte數進行符號位擴展變成16位全是1的char類型,由於char類型是無符號位,因此得到的結果是一個16爲全是1的無符號數,十進制是正的65536。

16位無符號的char類型轉換爲32位有符號的int類型時,只保留數值,不進行符號位擴展,因此的得到是一個高16位爲0,低16位爲1的int數,即65536.

結論:

char是僅有的無符號整數,在進行類型轉換時要特別小心,如果進行char類型向更寬數據類型轉換爲請注意以下兩條規則:

(1).如果將char類型c向更寬類型int轉換時不希望有符號擴展,請使用以下兩種方法:

使用位掩碼:int i = c & 0xffff;

直接賦值:int i = c;

(2).如果將char類型c向更寬類型int轉換時希望有符號擴展,請先將char轉換爲同樣寬度且有符號的short類型:

int i = (short)c;

如果將byte類型數據b向char類型轉換時,不希望符號擴展,則考慮使用符號位掩碼:

char c = (char) (0xff & b);


6.不使用臨時變量進行兩個數交換:

正常的使用臨時變量交互兩個變量值的例子代碼如下:

public static void main(String[] args) {
		int x = 1984;
		int y = 2001;
		int tmp = x;
		x = y;
		y = tmp;
		System.out.println("x=" + x + ",y=" + y);
	}
整個程序正常運行結果爲:x=2001,y=1984。

在C/C++中,很多人經常使用異或運算來進行兩個數交互,達到不使用臨時變量的目的,使用java重寫的代碼如下:

public static void main(String[] args) {
		int x = 1984;
		int y = 2001;
		x ^= y ^= x ^= y;
		System.out.println("x=" + x + ",y=" + y);
	}
但是在Java中,運行結果爲:x=0,y=1984。

原因:

在以前計算機硬件比較落後的情況下,CPU只有少數的寄存器,通過利用異或操作符的屬性(x ^ y ^ x)== y到達避免使用臨時變量的目的,具體運算過程分解如下:

x = x ^ y;

y = y ^ x;// y = y ^ (x ^ y) = x;

x = y ^ x;// x = x ^ (x ^ y) = y;

java語言規範中操作符的操作數是從左向右求值的,爲了求表達式x ^= expr的值,x的值是在計算expr之前被提取的,並且這兩個值的異或結果被賦值給變量x。

對於表達式x ^= y ^= x ^= y,x的值被提取兩次,每次在表達式中出現時都被提取一次,但是兩次提取都發生在所有的賦值操作前,詳細分解如下:

int tmp1 = x;//x在表達式中第一次出現

int tmp2 = y;//y在表達式中第一次出現

int tmp3 = x ^ y;//計算最左邊的x ^ y

x = tmp3;//最後一個賦值:將x ^ y存儲在x中

y = tmp2 ^ tmp3;//第二個賦值:將y ^ (x ^ y)即原始的x值存儲到y中

x = tmp1 ^ y;//第一個賦值:將x ^ x爲0的值存儲到x中

在C/C++中沒有指定表達式的計算順序,當編譯表達式x ^= expr時,許多C/C++編譯器都是在計算expr之後才提取x的值,因此可以正常工作。

結論:

如果想在java中不是有臨時變量交換兩個數的值,可以使用如下的代碼:

public static void main(String[] args) {
		int x = 1984;
		int y = 2001;
		y = (x ^= (y ^= x)) ^ y;
		System.out.println("x=" + x + ",y=" + y);
	}

不過不推薦這種用法,代碼讀起來不直觀明白,並且運行速度也不見得比使用臨時變量速度快。


7.三目運算符表達式:

問題:

下面一段程序代碼的的輸出結果應該是什麼?

public static void main(String[] args) {
		char x = 'X';
		int i = 0;
		System.out.println(true ? x : 0);
		System.out.println(true ? x : 65536);
		System.out.println(true ? x : i);
		System.out.println(false ? 0 : x);
		System.out.println(false ? i : x);
	}
我們期望的輸出是:

X
X
X
X
X
但是真正輸入的結果是:

X
88
88
X
88
原因:

之所以造成奇怪的輸出結果原因是三目運算符條件表達式對於第二個第三個操作數類型的規範如下:

(1).如果第二個和第三個操作數具有相同的類型,則該類型就是條件表達式的類型。

(2).如果兩個操作數的一個操作數的類型是T,T表示byte,short或者char類型,而另一個操作數是int類型的常量表達式,且該常量值是可以用T表示的,那麼條件表達式的值類型就是T。

(3).否則,將對操作數類型進行數據類型擴展提升,而條件表達式值的類型就是第二個和第三個操作數類型被擴展提升後的類型。

通過上面三條規範,我們就很容易明白上面程序的輸出結果了:

第1和第4個輸出:.由於0是int類型常量,且可以被char類型表示,適用於第二條規範,所以第一個輸入就char類型的X.

第2個輸出:.65536雖然是int類型的常量,但是超出了char類型的表示範圍,適用於第三條規範,因此將類型提升爲int,char類型的X提升爲int之後值爲88.

第3和第5個輸出:.i是int類型的變量,適用於第三條規範,因此將類型提升爲int,char類型的X提升爲int之後值爲88.

結論:

寫程序中,最好在條件表達式中使用類型相同的第二個和第三個操作數,否則可能輸出令你意想不到的結果。


8.複合賦值表達式不等價於簡單賦值表達式:

問題:

下面的4行程序哪一行有錯:

public static void main(String[] args) {
		short x = 0;//1
		int i = 123456;//2
		x = x + i;//3
		x += i;//4
	}
乍一看都沒有問題,但是如果把這段程序編譯,或者放在自動編譯的IDE中,第三行就會立刻報錯:Type mismatch: cannot convert from int to short.

原因:

很多人認爲簡單賦值表達式x = x + i和複合賦值表達式x += i是完全等價的,複合賦值表達式只不過是簡單賦值表達式的簡寫形式,但是通過上面的代碼,我們看到在簡單表達式編譯出錯的情況下,符合賦值表達式沒有錯誤,因此這二者不是簡單的完全等價。

java語言規範中定義複合賦值表達式爲:E1 op = E2等價於簡單賦值表達式:E1 = (T)((E1) op (E2)),其中T是E1的類型,因此符合賦值表達式會自動地將它們所執行的計算結果轉型爲其左側變量的類型,如果結果的類型與該變量類型相同,則該類型轉換不會造成任何影響,如果結果類型比該變量的類型要寬,則複合賦值表達式會自動進行一個窄化原始類型的類型轉換操作。

可以使用下面的程序來驗證類型轉換:

public static void main(String[] args) {
		short x = 0;
		int i = 123456;
		System.out.println(x += i);
	}
輸出結果爲:-7616。

相對應複合賦值表達式的類型轉換,簡單表達式x = x + i要將一個int類型的數值賦值給一個short類型的變量,由於類型不匹配,如果不進行顯式的強制類型轉換,編譯器就會報類型不匹配錯誤,因此不能通過編譯。

結論:

雖然簡單賦值表達式不能通過編譯,但是編譯錯誤也告訴了程序員問題所在,複合賦值表達式雖然可以正常運行,但是在上面程序中有丟失了計算精度,造成了令人意想不到的運算結果。因此在使用複合賦值表達式時,一定要注意左側變量的數據類型大於等於計算結果數據類型,避免類型窄化丟失精度的問題。


9.簡單賦值表達式不等價於複合賦值表達式:

下面的程序哪一行有錯誤:

public static void main(String[] args) {
		Object x = "Buy";//1
		String i = "Effective java";//2
		x = x + i;//3
		x += i;//4
	}
這個問題和第8個問題類似,只是這次是相反,複合賦值表達式出錯,而簡單賦值表達式正確。

沒錯,第4行會報錯:The operator += is undefined for the argument type(s) Object, String。

原因:

java中複合賦值表達式的限制條件爲:左右兩個操作數是原始類型,原始類型的包裝類型,對於對象類型的例外是:如果左側變量是String類型,則右側操作數可以是任意類型,表達式將執行字符串連接操作,但是不允許左側是非原始類型包裝類型和字符串的其他對象類型數據。

結論:

不能簡單認爲複合賦值表達式和簡單賦值表達式是完全等價的,如果真的想執行字符串和對象類型的複合賦值表達式,請將字符串類型放在左邊。

發佈了185 篇原創文章 · 獲贊 63 · 訪問量 146萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章