Java理論與實踐
您的小數點在哪?
——使用浮點數和小數中的技巧和陷阱
作者
Brian Goetz 本文選自
IBM DW中國網站
年
月
日
許多程序員在其整個開發生涯中都不曾使用定點或浮點數
可能的例外是
偶爾在計時測試或基准測試程序中會用到
Java 語言和類庫支持兩類非整數類型 — IEEE
浮點(float 和 double
包裝類(wrapper class)為 Float 和 Double)
以及任意精度的小數(java
math
BigDecimal)
在本月的 Java 理論和實踐中
Brian Goetz 探討了在 Java 程序中使用非整數類型時一些常碰到的陷阱和
gotcha
雖然幾乎每種處理器和編程語言都支持浮點運算
但大多數程序員很少注意它
這容易理解 — 我們中大多數很少需要使用非整數類型
除了科學計算和偶爾的計時測試或基准測試程序
其它情況下幾乎都用不著它
同樣
大多數開發人員也容易忽略 java
math
BigDecimal 所提供的任意精度的小數 — 大多數應用程序不使用它們
然而
在以整數為主的程序中有時確實會出人意料地需要表示非整型數據
例如
JDBC 使用 BigDecimal 作為 SQL DECIMAL 列的首選互換格式
IEEE 浮點
Java 語言支持兩種基本的浮點類型
float 和 double
以及與它們對應的包裝類 Float 和 Double
它們都依據 IEEE
標准
該標准為
位浮點和
位雙精度浮點二進制小數定義了二進制標准
IEEE
用科學記數法以底數為
的小數來表示浮點數
IEEE 浮點數用
位表示數字的符號
用
位來表示指數
用
位來表示尾數
即小數部分
作為有符號整數的指數可以有正負之分
小數部分用二進制(底數
)小數來表示
這意味著最高位對應著值 ?(
)
第二位對應著 ?(
)
依此類推
對於雙精度浮點數
用
位表示指數
位表示尾數
IEEE 浮點值的格式如圖
所示
IEEE
浮點數的格式
因為用科學記數法可以有多種方式來表示給定數字
所以要規范化浮點數
以便用底數為
並且小數點左邊為
的小數來表示
按照需要調節指數就可以得到所需的數字
所以
例如
數
可以表示為尾數為
指數為
除了編碼所允許的值的標准范圍(對於 float
從
e
到
e+
)
還有一些表示無窮大
負無窮大
和 NaN(它代表
不是一個數字
)的特殊值
這些值的存在是為了在出現錯誤條件(譬如算術溢出
給負數開平方根
除以
等)下
可以用浮點值集合中的數字來表示所產生的結果
這些特殊的數字有一些不尋常的特征
例如
和
是不同值
但在比較它們是否相等時
被認為是相等的
用一個非零數去除以無窮大的數
結果等於
特殊數字 NaN 是無序的
使用 ==
< 和 > 運算符將 NaN 與其它浮點值比較時
結果為 false
如果 f 為 NaN
則即使 (f == f) 也會得到 false
如果想將浮點值與 NaN 進行比較
則使用 Float
isNaN() 方法
表
顯示了無窮大和 NaN 的一些屬性
表
特殊浮點值的屬性
表達式 結果
Math
sqrt(
)
> NaN
/
> NaN
/
> 無窮大
/
> 負無窮大
NaN +
> NaN
無窮大 +
> 無窮大
無窮大 + 無窮大
> 無窮大
NaN >
> false
NaN ==
> false
NaN <
> false
NaN == NaN
> false
==
> true
基本浮點類型和包裝類浮點有不同的比較行為
使事情更糟的是
在基本 float 類型和包裝類 Float 之間
用於比較 NaN 和
的規則是不同的
對於 float 值
比較兩個 NaN 值是否相等將會得到 false
而使用 Float
equals() 來比較兩個 NaN Float 對象會得到 true
造成這種現象的原因是
如果不這樣的話
就不可能將 NaN Float 對象用作 HashMap 中的鍵
類似的
雖然
和
在表示為浮點值時
被認為是相等的
但使用 pareTo() 來比較作為 Float 對象的
和
時
會顯示
小於
浮點中的危險
由於無窮大
NaN 和
的特殊行為
當應用浮點數時
可能看似無害的轉換和優化實際上是不正確的
例如
雖然好象
f 很明顯等於
f
但當 f 為
時
這是不正確的
還有其它類似的 gotcha
表
顯示了其中一些 gotcha
表
無效的浮點假定
這個表達式…… 不一定等於…… 當……
f
f f 為
f < g ! (f >= g) f 或 g 為 NaN
f == f true f 為 NaN
f + g
g f g 為無窮大或 NaN
捨入誤差
浮點運算很少是精確的
雖然一些數字(譬如
)可以精確地表示為二進制(底數
)小數(因為
等於
)
但其它一些數字(譬如
)就不能精確的表示
因此
浮點運算可能導致捨入誤差
產生的結果接近 — 但不等於 — 您可能希望的結果
例如
下面這個簡單的計算將得到
而不是
double s=
;
for (int i=
; i<
; i++)
s +=
;
System
out
println(s);
類似的
*
相乘所產生的結果不等於
自身加
次所得到的結果
當將浮點數強制轉換成整數時
產生的捨入誤差甚至更嚴重
因為強制轉換成整數類型會捨棄非整數部分
甚至對於那些
看上去似乎
應該得到整數值的計算
也存在此類問題
例如
下面這些語句
double d =
*
;
System
out
println(d);
System
out
println((int) (d *
));
將得到以下輸出
這可能不是您起初所期望的
浮點數比較指南
由於存在 NaN 的不尋常比較行為和在幾乎所有浮點計算中都不可避免地會出現捨入誤差
解釋浮點值的比較運算符的結果比較麻煩
最好完全避免使用浮點數比較
當然
這並不總是可能的
但您應該意識到要限制浮點數比較
如果必須比較浮點數來看它們是否相等
則應該將它們差的絕對值同一些預先選定的小正數進行比較
這樣您所做的就是測試它們是否
足夠接近
(如果不知道基本的計算范圍
可以使用測試
abs(a/b
) < epsilon
這種方法比簡單地比較兩者之差要更准確)
甚至測試看一個值是比零大還是比零小也存在危險 —
以為
會生成比零略大值的計算事實上可能由於積累的捨入誤差會生成略微比零小的數字
NaN 的無序性質使得在比較浮點數時更容易發生錯誤
當比較浮點數時
圍繞無窮大和 NaN 問題
一種避免 gotcha 的經驗法則是顯式地測試值的有效性
而不是試圖排除無效值
在清單
中
有兩個可能的用於特性的 setter 的實現
該特性只能接受非負數值
第一個實現會接受 NaN
第二個不會
第二種形式比較好
因為它顯式地檢測了您認為有效的值的范圍
清單
需要非負浮點值的較好辦法和較差辦法
// Trying to test by exclusion
this doesn
t catch NaN or infinity
public void setFoo(float foo) {
if (foo <
)
throw new IllegalArgumentException(Float
toString(f));
this
foo = foo;
}
// Testing by inclusion
this does catch NaN
public void setFoo(float foo) {
if (foo >=
&& foo < Float
INFINITY)
this
foo = foo;
else
throw new IllegalArgumentException(Float
toString(f));
}
不要用浮點值表示精確值
一些非整數值(如幾美元和幾美分這樣的小數)需要很精確
浮點數不是精確值
所以使用它們會導致捨入誤差
因此
使用浮點數來試圖表示象貨幣量這樣的精確數量不是一個好的想法
使用浮點數來進行美元和美分計算會得到災難性的後果
浮點數最好用來表示象測量值這類數值
這類值從一開始就不怎麼精確
用於較小數的 BigDecimal
從 JDK
起
Java 開發人員就有了另一種數值表示法來表示非整數
BigDecimal
BigDecimal 是標准的類
在編譯器中不需要特殊支持
它可以表示任意精度的小數
並對它們進行計算
在內部
可以用任意精度任何范圍的值和一個換算因子來表示 BigDecimal
換算因子表示左移小數點多少位
從而得到所期望范圍內的值
因此
用 BigDecimal 表示的數的形式為
scale
unscaledValue*
用於加
減
乘和除的方法給 BigDecimal 值提供了算術運算
由於 BigDecimal 對象是不可變的
這些方法中的每一個都會產生新的 BigDecimal 對象
因此
因為創建對象的開銷
BigDecimal 不適合於大量的數學計算
但設計它的目的是用來精確地表示小數
如果您正在尋找一種能精確表示如貨幣量這樣的數值
則 BigDecimal 可以很好地勝任該任務
所有的 equals 方法都不能真正測試相等
如浮點類型一樣
BigDecimal 也有一些令人奇怪的行為
尤其在使用 equals() 方法來檢測數值之間是否相等時要小心
equals() 方法認為
兩個表示同一個數但換算值不同(例如
和
)的 BigDecimal 值是不相等的
然而
compareTo() 方法會認為這兩個數是相等的
所以在從數值上比較兩個 BigDecimal 值時
應該使用 compareTo() 而不是 equals()
另外還有一些情形
任意精度的小數運
From:http://tw.wingwit.com/Article/program/Java/JSP/201311/19617.html