Java Puzzlers(一 表達式計算)
1 奇數判斷
誤:public static boolean isOdd(int i) { return i % 2 == 1; } //沒有考慮到負奇數的情況
正:return i % 2 != 0; 更好的性能:return (i & 1) != 0;
總結:求余操作需要考慮符號!
2 浮點數計算
public static void main(String args[]) { System.out.println(2.00 - 1.10); } //天真以為得到0.90
如果熟悉Double.toString 的文檔,估計會覺得 double 會轉為string,程序會打印出足夠區分double值的小數部分,小數點前或后面至少一位。這樣說來應該是0.9,可惜運行程序發現是 0.8999999999999999。問題是數字1.1不能被double準確表示!只能用最接近的double值表示。遺憾的是結果不是最接近0.9的double值。更普遍的看這問題是:不是所有的十進制數都能用二進制浮點數準確的表示 。如果用jdk5或以后版本,你可能會使用printf來準確設置:
// Poor solution - still uses binary floating-point!
System.out.printf("%.2f%n", 2.00 - 1.10);
現在打印出來是正確的了,但治標不治本:它仍然使用的是double運算(二進制浮點),浮點計算在大范圍內提供近似計算,但不總是產生準確的結果。二進制浮點數特別不適合金融計算,因為他不可能表示0.1——或任何10的負冪——exactly as a finite-length binary fraction。
一種解決辦法是使用基本類型,比如int long,然后擴大操作數倍數做計算。如果用這種流程,確保基本類型足夠大來表示你所有你用到的數據,這個例子中,int足夠了System.out.println((200 - 110) + " cents");
另一種辦法使用BigDecimal,他進行準確的十進制計算,他還能通過JDBC和SQL的DECIMAL類型合作。有一個箴言:總是使用BigDecimal(String)構造器,絕不使用BigDecimal(double).后面這個構造函數用參數的準確值創建一個實例:new BigDecimal(.1)返回一個BigDecimal表示0.1000000000000000055511151231257827021181583404541015625。正確使用會得到預期結果0.90: System.out.println(new BigDecimal("2.00"). subtract(new BigDecimal("1.10"))); 這個例子不是特別漂亮,java沒有給BigDecimal提供語言學上的支持,BigDecimal也可能比使用基本類型(對大量使用十進制計算的程序比較有用)更慢,大多數情況沒有這個需要。
總結:但需要準確答案的時候,避免使用float and double;金融計算,使用int, long, or BigDecimal。對語言設計者來說,提供十進制計算的語言支持。一個方法是給操作符重載提供有限的支持,這樣計算操作符就能和數字引用類型比如BigDecimal一起工作?。另一種方法就是像COBOL and PL/I一樣,提供基本十進制類型。
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!問題出在計算MICROS_PER_DAY 時溢出了,雖然結果是滿足long的,但不滿足int。這個計算過程全部是按int 計算的,計算完之后才轉為long。因此很明顯計算過程中溢出。為什么會用int計算?因為因子都是int型的,Java沒有 target typing特性(就是根據結果的類型來確定計算過程所用類型)。解決這個問題很簡單,把第一個因子設置為long,這樣會強制所有以后的計算都用long進行。雖然很多地方都不需要這么做,但這是一個好習慣。
final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
我們得到一個教訓:和大數據打交道的時候,小心溢出!一個變量能裝得下結果,并不代表計算過程中會確保得到正確類型。
4 小學生都知道的事情
System.out.println(12345 + 5432l); // 毫無疑問的66666? 看仔細了!輸出17777
教訓:使用long的時候用大寫的L,絕不用小寫的l,類似的避免用l作為變量名。很難看出輸出的是1還是l
// Bad code - uses el (l) as a variable name
List<String> l = new ArrayList<String>();
l.add("Foo");
System.out.println(1);
5 十六進制的快樂
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe)); //輸出cafebabe,最左邊的1丟了!
十進制有一個十六或八進制都沒有的優點:數字都是正的,想表達負數需要一個負號。這樣的話寫十進制的int或long,不管正負都很方便。十六或八進制就不這樣了,必須由高位來決定正負。這個例子中,0xcafebabe 是一個int常量,最高位是1,因此是負數=十進制 -889275714。這里還有一個混合類型計算的額外操作:左邊操作數是long,右邊是int,計算時Java通過widening primitive conversion 把int變為long,再加這兩個long。因為int是有符號整型,轉變執行了一個符號擴展:把負的int值提升為數值相等的long值。右邊的0xcafebabe被提升為long值 0xffffffffcafebabeL,再加到左邊0x100000000L上。當被看作int型的時候,0xcafebabe擴展出來的高32位是-1,而左邊操作數高32位是1,相加之后為0,這解釋了為什么最高位的1丟失。解決方法是把右邊的操作數也寫上long,這樣就避免了符號擴展的破壞。
System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));
教訓:考慮十六或八進制自身帶正負,混合類型計算讓人迷惑。為避免出錯,最好不要使用混合類型計算。對語言設計者來說,考慮支持無符號整數類型來去掉符號擴展的可能。有人爭論十六或八進制負數應該被禁止,但對于程序員來說非常不好,他們經常使用十六進制來表示符號沒有意義的數值。
6 多重映射
System.out.println((int) (char) (byte) -1);
以int 類型的-1開始,映射到byte,到char,最后返回int。第一次32位變到8位,到16位,最后回到32位。最后發現值并沒有回到原始!輸出65535
問題來自映射時的符號擴展問題。int值-1的所有32位都是1,轉為8位byte很直觀,只留下低八位就行,仍然是-1.轉char的時候,就要小心了,byte是有符號的,char無符號。通常有可能保留數值的同時把一個整型轉到更“寬”的類型,但不可能用char來表示一個負的byte值。Therefore, the conversion from byte to char is not considered a widening primitive conversion [JLS 5.1.2], but a widening and narrowing primitive conversion [JLS 5.1.4]: The byte is converted to an int and the int to a char。看起來較復雜,但有一個簡單規則描述窄變寬轉換時的符號擴展:原始值有符號就做符號擴展;不管轉換成什么類型,char只做零擴展。因為byte是有符號的,byte -1轉成char會有符號擴展。結果是全1的16位,也就是 216 – 1或65,535。char到int寬擴展,規則告訴我們做零擴展。int類型的結果是65535。雖然規則簡單,但最好不要寫依賴這規則的程序。如果你是寬轉換到char,或從char轉換(char總是無符號整數),最好顯式說明。
如果從char類型的c寬轉換,并且不想符號擴展,雖然不需要,但為了清晰可以這樣:
int i = c & 0xffff;
還可以寫注釋:
int i = c; // Sign extension is not performed
如果從char類型的c寬轉換,并且想符號擴展,強制char到short(寬度一樣但有符號)
int i = (short) c; // Cast causes sign extension
byte到char,不要符號,必須用位屏蔽抑制他,這是慣例不用注釋(0xff這種0x開頭的默認是int類型的?)
char c = (char) (b & 0xff);
byte to a char ,要符號,寫注釋
char c = (char) b; // Sign extension is performed
這一課很簡單:如果你不能清晰看出程序在干什么,他可能就沒有按你希望的在運行。拼命尋求清晰,雖然整數轉換的符號擴展規則簡單,但大多程序員不知道。如果你的程序依賴于他,讓你的意圖明顯。
7 交換美味
在一個簡單表達式中,不要對一個變量賦值超過一次。更普遍的說,不要用“聰明”的程序技巧。
8 Dos Equis
char x = 'X';
int i = 0;
System.out.print(true ? x : 0);
System.out.print(false ? i : x);
輸出XX?可惜輸出的是X88。注意第二三個操作數類型不一樣,第5點說過,混合類型計算讓人迷惑!條件表達式中是最明顯的地方。雖然覺得兩個表達式結果應該相同,畢竟他們類型相似,只是位置相反而已,但結果并不是這樣。
決定條件表達式結果類型的規則很多,但有三個關鍵點:
1 如果第二三個操作數類型一樣,表達式也是這個類型,這樣就避免了混合類型計算。
2 3 復雜略過 總之第一個表達式是調用了PrintStream.print(char),第二個是PrintStream.print(int) 造成結果不同
總結:最好在條件表達式中第二三個操作數用同一種類型
9
x += i; // 等同于x = x + i;?
compound assignment expressions automatically cast the result of the computation they perform to the type of the variable on their left-hand side// 暗含映射
例如 short x = 0;int i = 123456;
x += i; // –7,616,int值123456太大,short裝不下,高位兩個字節被去掉
x = x + i; // 編譯錯誤- "possible loss of precision"
為避免危險,不要在byte, short, or char上面用復合賦值符。當在int上用時,確保右邊不是long, float, or double類型。在float上用,確保右邊不是double。
10 復合賦值符需要兩邊操作數都為基本類型或boxed primitives,如int ,Integer。有一例外:+= 左邊為String的話,允許右邊為任意類型。這時做的是字符串拼接操作。
Object x = "Buy ";
String i = "Effective Java!";
x = x + i; //x+i 為String,和Object兼容,因此表達式正確
x += i; //非法左邊不是String
注意返回類型:
誤:public static boolean isOdd(int i) { return i % 2 == 1; } //沒有考慮到負奇數的情況
正:return i % 2 != 0; 更好的性能:return (i & 1) != 0;
總結:求余操作需要考慮符號!
2 浮點數計算
public static void main(String args[]) { System.out.println(2.00 - 1.10); } //天真以為得到0.90
如果熟悉Double.toString 的文檔,估計會覺得 double 會轉為string,程序會打印出足夠區分double值的小數部分,小數點前或后面至少一位。這樣說來應該是0.9,可惜運行程序發現是 0.8999999999999999。問題是數字1.1不能被double準確表示!只能用最接近的double值表示。遺憾的是結果不是最接近0.9的double值。更普遍的看這問題是:不是所有的十進制數都能用二進制浮點數準確的表示 。如果用jdk5或以后版本,你可能會使用printf來準確設置:
// Poor solution - still uses binary floating-point!
System.out.printf("%.2f%n", 2.00 - 1.10);
現在打印出來是正確的了,但治標不治本:它仍然使用的是double運算(二進制浮點),浮點計算在大范圍內提供近似計算,但不總是產生準確的結果。二進制浮點數特別不適合金融計算,因為他不可能表示0.1——或任何10的負冪——exactly as a finite-length binary fraction。
一種解決辦法是使用基本類型,比如int long,然后擴大操作數倍數做計算。如果用這種流程,確保基本類型足夠大來表示你所有你用到的數據,這個例子中,int足夠了System.out.println((200 - 110) + " cents");
另一種辦法使用BigDecimal,他進行準確的十進制計算,他還能通過JDBC和SQL的DECIMAL類型合作。有一個箴言:總是使用BigDecimal(String)構造器,絕不使用BigDecimal(double).后面這個構造函數用參數的準確值創建一個實例:new BigDecimal(.1)返回一個BigDecimal表示0.1000000000000000055511151231257827021181583404541015625。正確使用會得到預期結果0.90: System.out.println(new BigDecimal("2.00"). subtract(new BigDecimal("1.10"))); 這個例子不是特別漂亮,java沒有給BigDecimal提供語言學上的支持,BigDecimal也可能比使用基本類型(對大量使用十進制計算的程序比較有用)更慢,大多數情況沒有這個需要。
總結:但需要準確答案的時候,避免使用float and double;金融計算,使用int, long, or BigDecimal。對語言設計者來說,提供十進制計算的語言支持。一個方法是給操作符重載提供有限的支持,
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!問題出在計算MICROS_PER_DAY 時溢出了,雖然結果是滿足long的,但不滿足int。這個計算過程全部是按int 計算的,計算完之后才轉為long。因此很明顯計算過程中溢出。為什么會用int計算?因為因子都是int型的,Java沒有 target typing特性(就是根據結果的類型來確定計算過程所用類型)。解決這個問題很簡單,把第一個因子設置為long,這樣會強制所有以后的計算都用long進行。雖然很多地方都不需要這么做,但這是一個好習慣。
final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
我們得到一個教訓:和大數據打交道的時候,小心溢出!一個變量能裝得下結果,并不代表計算過程中會確保得到正確類型。
4 小學生都知道的事情
System.out.println(12345 + 5432l); // 毫無疑問的66666? 看仔細了!輸出17777
教訓:使用long的時候用大寫的L,絕不用小寫的l,類似的避免用l作為變量名。很難看出輸出的是1還是l
// Bad code - uses el (l) as a variable name
List<String> l = new ArrayList<String>();
l.add("Foo");
System.out.println(1);
5 十六進制的快樂
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe)); //輸出cafebabe,最左邊的1丟了!
十進制有一個十六或八進制都沒有的優點:數字都是正的,想表達負數需要一個負號。這樣的話寫十進制的int或long,不管正負都很方便。十六或八進制就不這樣了,必須由高位來決定正負。這個例子中,0xcafebabe 是一個int常量,最高位是1,因此是負數=十進制 -889275714。這里還有一個混合類型計算的額外操作:左邊操作數是long,右邊是int,計算時Java通過widening primitive conversion 把int變為long,再加這兩個long。因為int是有符號整型,轉變執行了一個符號擴展:把負的int值提升為數值相等的long值。右邊的0xcafebabe被提升為long值 0xffffffffcafebabeL,再加到左邊0x100000000L上。當被看作int型的時候,0xcafebabe擴展出來的高32位是-1,而左邊操作數高32位是1,相加之后為0,這解釋了為什么最高位的1丟失。解決方法是把右邊的操作數也寫上long,這樣就避免了符號擴展的破壞。
System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));
教訓:考慮十六或八進制自身帶正負,混合類型計算讓人迷惑。為避免出錯,最好不要使用混合類型計算。對語言設計者來說,考慮支持無符號整數類型來去掉符號擴展的可能。有人爭論十六或八進制負數應該被禁止,但對于程序員來說非常不好,他們經常使用十六進制來表示符號沒有意義的數值。
6 多重映射
System.out.println((int) (char) (byte) -1);
以int 類型的-1開始,映射到byte,到char,最后返回int。第一次32位變到8位,到16位,最后回到32位。最后發現值并沒有回到原始!輸出65535
問題來自映射時的符號擴展問題。int值-1的所有32位都是1,轉為8位byte很直觀,只留下低八位就行,仍然是-1.轉char的時候,就要小心了,byte是有符號的,char無符號。通常有可能保留數值的同時把一個整型轉到更“寬”的類型,但不可能用char來表示一個負的byte值。Therefore, the conversion from byte to char is not considered a widening primitive conversion [JLS 5.1.2], but a widening and narrowing primitive conversion [JLS 5.1.4]: The byte is converted to an int and the int to a char。看起來較復雜,但有一個簡單規則描述窄變寬轉換時的符號擴展:原始值有符號就做符號擴展;不管轉換成什么類型,char只做零擴展。因為byte是有符號的,byte -1轉成char會有符號擴展。結果是全1的16位,也就是 216 – 1或65,535。char到int寬擴展,規則告訴我們做零擴展。int類型的結果是65535。雖然規則簡單,但最好不要寫依賴這規則的程序。如果你是寬轉換到char,或從char轉換(char總是無符號整數),最好顯式說明。
如果從char類型的c寬轉換,并且不想符號擴展,雖然不需要,但為了清晰可以這樣:
int i = c & 0xffff;
還可以寫注釋:
int i = c; // Sign extension is not performed
如果從char類型的c寬轉換,并且想符號擴展,強制char到short(寬度一樣但有符號)
int i = (short) c; // Cast causes sign extension
byte到char,不要符號,必須用位屏蔽抑制他,這是慣例不用注釋(0xff這種0x開頭的默認是int類型的?)
char c = (char) (b & 0xff);
byte to a char ,要符號,寫注釋
char c = (char) b; // Sign extension is performed
這一課很簡單:如果你不能清晰看出程序在干什么,他可能就沒有按你希望的在運行。拼命尋求清晰,雖然整數轉換的符號擴展規則簡單,但大多程序員不知道。如果你的程序依賴于他,讓你的意圖明顯。
7 交換美味
在一個簡單表達式中,不要對一個變量賦值超過一次。更普遍的說,不要用“聰明”的程序技巧。
8 Dos Equis
char x = 'X';
int i = 0;
System.out.print(true ? x : 0);
System.out.print(false ? i : x);
輸出XX?可惜輸出的是X88。注意第二三個操作數類型不一樣,第5點說過,混合類型計算讓人迷惑!條件表達式中是最明顯的地方。雖然覺得兩個表達式結果應該相同,畢竟他們類型相似,只是位置相反而已,但結果并不是這樣。
決定條件表達式結果類型的規則很多,但有三個關鍵點:
1 如果第二三個操作數類型一樣,表達式也是這個類型,這樣就避免了混合類型計算。
2 3 復雜略過 總之第一個表達式是調用了PrintStream.print(char),第二個是PrintStream.print(int) 造成結果不同
總結:最好在條件表達式中第二三個操作數用同一種類型
9
x += i; // 等同于x = x + i;?
compound assignment expressions automatically cast the result of the computation they perform to the type of the variable on their left-hand side// 暗含映射
例如 short x = 0;int i = 123456;
x += i; // –7,616,int值123456太大,short裝不下,高位兩個字節被去掉
x = x + i; // 編譯錯誤- "possible loss of precision"
為避免危險,不要在byte, short, or char上面用復合賦值符。當在int上用時,確保右邊不是long, float, or double類型。在float上用,確保右邊不是double。
10 復合賦值符需要兩邊操作數都為基本類型或boxed primitives,如int ,Integer。有一例外:+= 左邊為String的話,允許右邊為任意類型。這時做的是字符串拼接操作。
Object x = "Buy ";
String i = "Effective Java!";
x = x + i; //x+i 為String,和Object兼容,因此表達式正確
x += i; //非法左邊不是String
注意返回類型:
The arithmetic, increment and decrement, bitwise, and shift operators return a double if at least one of the operands is a double. Otherwise, they return a float if at least one of the operands is a float. Otherwise, they return a long if at least one of the operands is a long. Otherwise, they return an int, even if both operands are byte, short, or char types that are narrower than int.
posted on 2010-10-13 13:27 yuxh 閱讀(177) 評論(0) 編輯 收藏 所屬分類: jdk