歡迎您光臨本站 註冊首頁

使用浮點數和小數中的技巧和陷阱

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0
簡介: 許多程序員在其整個開發生涯中都不曾使用定點或浮點數,可能的例外是,偶爾在計時測試或基準測試程序中會用到.Java語言和類庫支持兩類非整數類型 ― IEEE 754 浮點( 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 754 標準,該標準為 32 位浮點和 64 位雙精度浮點二進位小數定義了二進位標準.
IEEE 754 用科學記數法以底數為 2 的小數來表示浮點數.IEEE 浮點數用 1 位表示數字的符號,用 8 位來表示指數,用 23 位來表示尾數,即小數部分.作為有符號整數的指數可以有正負之分.小數部分用二進位(底數 2)小數來表示,這意味著最高位對應著值 ?(2 -1),第二位對應著 ?(2 -2),依此類推.對於雙精度浮點數,用 11 位表示指數,52 位表示尾數.IEEE 浮點值的格式如圖 1 所示.
圖 1. IEEE 754 浮點數的格式






用科學記數法可以有多種方式來表示給定數字,要規範化浮點數,以便用底數為 2 並且小數點左邊為 1 的小數來表示,按照需要調節指數就可以得到所需的數字.,例如,數 1.25 可以表示為尾數為 1.01,指數為 0: (-1) 0*1.01 2*2 0
數 10.0 可以表示為尾數為 1.01,指數為 3: (-1) 0*1.01 2*2 3
特殊數字
除了編碼所允許的值的標準範圍(對於 float ,從 1.4e-45 到 3.4028235e 38),還有一些表示無窮大、負無窮大、 -0 和 NaN(它代表「不是一個數字」)的特殊值.這些值的存在是為了在出現錯誤條件(譬如算術溢出,給負數開平方根,除以 0 等)下,可以用浮點值集合中的數字來表示所產生的結果.


這些特殊的數字有一些不尋常的特徵.例如, 0 和 -0 是不同值,但在比較它們是否相等時,被認為是相等的.用一個非零數去除以無窮大的數,結果等於 0 .特殊數字 NaN 是無序的;使用 == 、 < 和 > 運算符將 NaN 與其它浮點值比較時,結果為 false .如果 f 為 NaN,則即使 (f == f) 也會得到 false .如果想將浮點值與 NaN 進行比較,則使用 Float.isNaN() 方法.表 1 顯示了無窮大和 NaN 的一些屬性.
表 1. 特殊浮點值的屬性
表達式 結果
Math.sqrt(-1.0) -> NaN
0.0 / 0.0 -> NaN
1.0 / 0.0 -> 無窮大
-1.0 / 0.0 -> 負無窮大
NaN 1.0 -> NaN
無窮大 1.0 -> 無窮大
無窮大 無窮大 -> 無窮大
NaN > 1.0 -> false
NaN == 1.0 -> false
NaN < 1.0 -> false
NaN == NaN -> false
0.0 == -0.01 -> true
基本浮點類型和包裝類浮點有不同的比較行為
使事情更糟的是,在基本 float 類型和包裝類 Float 之間,用於比較 NaN 和 -0 的規則是不同的.對於 float 值,比較兩個 NaN 值是否相等將會得到 false ,而使用 Float.equals() 來比較兩個 NaN Float 對象會得到 true .造成這種現象的原因是,如果不這樣的話,就不可能將 NaN Float 對象用作 HashMap 中的鍵.類似的,雖然 0 和 -0 在表示為浮點值時,被認為是相等的,但使用 Float.compareTo() 來比較作為 Float 對象的 0 和 -0 時,會顯示 -0 小於 0 .
浮點中的危險
由於無窮大、NaN 和 0 的特殊行為,當應用浮點數時,可能看似無害的轉換和優化實際上是不正確的.例如,雖然好象 0.0-f 很明顯等於 -f ,但當 f 為 0 時,這是不正確的.還有其它類似的 gotcha,表 2 顯示了其中一些 gotcha.
表 2. 無效的浮點假定
這個表達式…… 不一定等於…… 當……
0.0 - f -f f 為 0
f < g ! (f >= g) f 或 g 為 NaN
f == f true f 為 NaN
f g - g f g 為無窮大或 NaN
舍入誤差
浮點運算很少是精確的.雖然一些數字(譬如 0.5 )可以精確地表示為二進位(底數 2)小數( 0.5 等於 2 -1),但其它一些數字(譬如 0.1 )就不能精確的表示.因此,浮點運算可能導致舍入誤差,產生的結果接近 ― 但不等於 ― 您可能希望的結果.例如,下面這個簡單的計算將得到 2.600000000000001 ,而不是 2.6 :
double s=0; for (int i=0; i<26; i ) s = 0.1; System.out.println(s);
類似的, .1*26 相乘所產生的結果不等於 .1 自身加 26 次所得到的結果.當將浮點數強制轉換成整數時,產生的舍入誤差甚至更嚴重,強制轉換成整數類型會捨棄非整數部分,甚至對於那些「看上去似乎」應該得到整數值的計算,也存在此類問題.例如,下面這些語句:


double d = 29.0 * 0.01; System.out.println(d); System.out.println((int) (d * 100));
將得到以下輸出:
0.29 28
這可能不是您起初所期望的.
浮點數比較指南
由於存在 NaN 的不尋常比較行為和在幾乎所有浮點計算中都不可避免地會出現舍入誤差,解釋浮點值的比較運算符的結果比較麻煩.
最好完全避免使用浮點數比較.當然,這並不總是可能的,但您應該意識到要限制浮點數比較.如果必須比較浮點數來看它們是否相等,則應該將它們差的絕對值同一些預先選定的小正數進行比較,這樣您所做的就是測試它們是否「足夠接近」.(如果不知道基本的計算範圍,可以使用測試 「abs(a/b - 1) < epsilon」,這種方法比簡單地比較兩者之差要更準確).甚至測試看一個值是比零大還是比零小也存在危險 ―「以為」會生成比零略大值的計算事實上可能由於積累的舍入誤差會生成略微比零小的數字.
NaN 的無序性質是的在比較浮點數時更容易發生錯誤.當比較浮點數時,圍繞無窮大和 NaN 問題,一種避免 gotcha 的經驗法則是顯式地測試值的有效性,而不是試圖排除無效值.在清單 1 中,有兩個可能的用於特性的 setter 的實現,該特性只能接受非負數值.第一個實現會接受 NaN,第二個不會.第二種形式比較好,它顯式地檢測了您認為有效的值的範圍.
清單 1. 需要非負浮點值的較好辦法和較差辦法
// Trying to test by exclusion -- this doesn't catch NaN or infinity public void setFoo(float foo) { if (foo < 0) throw new IllegalArgumentException(Float.toString(f)); this.foo = foo; } // Testing by inclusion -- this does catch NaN public void setFoo(float foo) { if (foo >= 0 && foo < Float.INFINITY) this.foo = foo; else throw new IllegalArgumentException(Float.toString(f)); }
不要用浮點值表示精確值
一些非整數值(如幾美元和幾美分這樣的小數)需要很精確.浮點數不是精確值,使用它們會導致舍入誤差.因此,使用浮點數來試圖表示象貨幣量這樣的精確數量不是一個好的想法.使用浮點數來進行美元和美分計算會得到災難性的後果.浮點數最好用來表示象測量值這類數值,這類值從一開始就不怎麼精確.
用於較小數的 BigDecimal
從 JDK 1.3 起,Java 開發人員就有了另一種數值表示法來表示非整數: BigDecimal . BigDecimal 是標準的類,在編譯器中不需要特殊支持,它可以表示任意精度的小數,並對它們進行計算.在內部,可以用任意精度任何範圍的值和一個換算因子來表示 BigDecimal ,換算因子表示左移小數點多少位,從而得到所期望範圍內的值.因此,用 BigDecimal 表示的數的形式為 unscaledValue*10 -scale .


用於加、減、乘和除的方法給 BigDecimal 值提供了算術運算.由於 BigDecimal 對象是不可變的,這些方法中的每一個都會產生新的 BigDecimal 對象.因此,創建對象的開銷, BigDecimal 不適合於大量的數學計算,但設計它的目的是用來精確地表示小數.如果您正在尋找一種能精確表示如貨幣量這樣的數值,則 BigDecimal 可以很好地勝任該任務.
所有的 equals 方法都不能真正測試相等
如浮點類型一樣, BigDecimal 也有一些令人奇怪的行為.尤其在使用 equals() 方法來檢測數值之間是否相等時要小心. equals() 方法認為,兩個表示同一個數但換算值不同(例如, 100.00 和 100.000 )的 BigDecimal 值是不相等的.然而, compareTo() 方法會認為這兩個數是相等的,在從數值上比較兩個 BigDecimal 值時,應該使用 compareTo() 而不是 equals() .
另外還有一些情形,任意精度的小數運算仍不能表示精確結果.例如, 1 除以 9 會產生無限循環的小數 .111111... .出於這個原因,在進行除法運算時, BigDecimal 可以讓您顯式地控制舍入. movePointLeft() 方法支持 10 的冪次方的精確除法.
使用 BigDecimal 作為互換類型
SQL-92 包括 DECIMAL 數據類型,它是用於表示定點小數的精確數字類型,它可以對小數進行基本的算術運算.一些 SQL 語言喜歡稱此類型為 NUMERIC 類型,其它一些 SQL 語言則引入了 MONEY 數據類型,MONEY 數據類型被定義為小數點右側帶有兩位的小數.
如果希望將數字存儲到資料庫中的 DECIMAL 欄位,或從 DECIMAL 欄位檢索值,則如何確保精確地轉換該數字?您可能不希望使用由 JDBC PreparedStatement 和 ResultSet 類所提供的 setFloat() 和 getFloat() 方法,浮點數與小數之間的轉換可能會喪失精確性.相反,請使用 PreparedStatement 和 ResultSet 的 setBigDecimal() 及 getBigDecimal() 方法.
對於 BigDecimal ,有幾個可用的構造函數.其中一個構造函數以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 String 表示作為輸入.要小心使用 BigDecimal(double) 構造函數,如果不了解它,會在計算過程中產生舍入誤差.請使用基於整數或 String 的構造函數.
構造 BigDecimal 數
對於 BigDecimal ,有幾個可用的構造函數.其中一個構造函數以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 String 表示作為輸入.要小心使用 BigDecimal(double) 構造函數,如果不了解它,會在計算過程中產生舍入誤差.請使用基於整數或 String 的構造函數.
如果使用 BigDecimal(double) 構造函數不恰當,在傳遞給 JDBC setBigDecimal() 方法時,會造成似乎很奇怪的 JDBC 驅動程序中的異常.例如,考慮以下 JDBC 代碼,該代碼希望將數字 0.01 存儲到小數欄位:


PreparedStatement ps = connection.prepareStatement("INSERT INTO Foo SET name=?, value=?"); ps.setString(1, "penny"); ps.setBigDecimal(2, new BigDecimal(0.01)); ps.executeUpdate();
在執行這段似乎無害的代碼時會拋出一些令人迷惑不解的異常(這取決於具體的 JDBC 驅動程序), 0.01 的雙精度近似值會導致大的換算值,這可能會使 JDBC 驅動程序或資料庫感到迷惑.JDBC 驅動程序會產生異常,但可能不會說明代碼實際上錯在哪裡,除非意識到二進位浮點數的局限性.相反,使用 BigDecimal("0.01") 或 BigDecimal(1, 2) 構造 BigDecimal 來避免這類問題,這兩種方法都可以精確地表示小數.
結束語
在 Java 程序中使用浮點數和小數充滿著陷阱.浮點數和小數不象整數一樣「循規蹈矩」,不能假定浮點計算一定產生整型或精確的結果,雖然它們的確「應該」那樣做.最好將浮點運算保留用作計算本來就不精確的數值,譬如測量.如果需要表示定點數(譬如,幾美元和幾美分),則使用 BigDecimal .


[火星人 ] 使用浮點數和小數中的技巧和陷阱已經有1615次圍觀

http://coctec.com/docs/java/show-post-59995.html