謎題2:找零時刻
請考慮下面這段話所描述的問題:
Tom在一家汽車配件商店購買了一個價值$1.10的火花塞,但是他錢包中都是兩美元一張的鈔票。如果他用一張兩美元的鈔票支付這個火花塞,那麽應該找給他多少零錢呢?
下面是一個試圖解決上述問題的程序,它會打印出什麽呢?
public class Change{
public static void main(String args[]){
System.out.println(2.00 - 1.10);
}
}
你可能會很天真地期望該程序能夠打印出0.90,但是它如何才能知道你想要打印小數點後兩位小數呢?
如果你對在Double.toString文檔中所設定的將double類型的值轉換為字符串的規則有所了解,你就會知道該程序打印出來的小數,是足以將double類型的值與最靠近它的臨近值區分出來的最短的小數,它在小數點之前和之後都至少有一位。因此,看起來,該程序應該打印0.9是合理的。
這麽分析可能顯得很合理,但是並不正確。如果你運行該程序,你就會發現它打印的是0.8999999999999999。
問題在於1.1這個數字不能被精確表示成為一個double,因此它被表示成為最接近它的double值。該程序從2中減去的就是這個值。遺憾的是,這個計算的結果並不是最接近0.9的double值。表示結果的double值的最短表示就是你所看到的打印出來的那個可惡的數字。
更一般地說,問題在於並不是所有的小數都可以用二進制浮點數來精確表示的。
如果你正在用的是JDK 5.0或更新的版本,那麽你可能會受其誘惑,通過使用printf工具來設置輸出精度的方訂正該程序:
//拙劣的解決方案——仍舊是使用二進制浮點數
System.out.printf("%.2f%n",2.00 - 1.10);
這條語句打印的是正確的結果,但是這並不表示它就是對底層問題的通用解決方案:它使用的仍舊是二進制浮點數的double運算。浮點運算在一個範圍很廣的值域上提供了很好的近似,但是它通常不能產生精確的結果。二進制浮點對於貨幣計算是非常不適合的,因為它不可能將0.1——或者10的其它任何次負冪——精確表示為一個長度有限的二進制小數
解決該問題的一種方式是使用某種整數類型,例如int或long,並且以分為單位來執行計算。如果你采納了此路線,請確保該整數類型大到足夠表示在程序中你將要用到的所有值。對這裏舉例的謎題來說,int就足夠了。下面是我們用int類型來以分為單位表示貨幣值後重寫的println語句。這個版本將打印出正確答案90分:
System.out.println((200 - 110) + "cents");
解決該問題的另一種方式是使用執行精確小數運算的BigDecimal。它還可以通過JDBC與SQL DECIMAL類型進行互操作。這裏要告誡你一點: 一定要用BigDecimal(String)構造器,而千萬不要用BigDecimal(double)。後一個構造器將用它的參數的“精確”值來創建一個實例:new BigDecimal(.1)將返回一個表示0.100000000000000055511151231257827021181583404541015625的BigDecimal。通過正確使用BigDecimal,程序就可以打印出我們所期望的結果0.90:
import java.math.BigDecimal;
public class Change1{
public static void main(String args[]){
System.out.println(new BigDecimal("2.00").
subtract(new BigDecimal("1.10")));
}
}
這個版本並不是十分地完美,因為Java並沒有為BigDecimal提供任何語言上的支持。使用BigDecimal的計算很有可能比那些使用原始類型的計算要慢一些,對某些大量使用小數計算的程序來說,這可能會成為問題,而對大多數程序來說,這顯得一點也不重要。
總之, 在需要精確答案的地方,要避免使用float和double;對於貨幣計算,要使用int、long或BigDecimal。對於語言設計者來說,應該考慮對小數運算提供語言支持。一種方式是提供對操作符重載的有限支持,以使得運算符可以被塑造為能夠對數值引用類型起作用,例如BigDecimal。另一種方式是提供原始的小數類型,就像COBOL與PL/I所作的一樣。
謎題2:找零時刻