請考慮下面這段話所描述的問題:
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 所作的一樣。
内容來至<<java解惑>>