學習JAVA浮點數必看文章!
雖然幾乎每種處理器和程式語言都支援浮點運算,但大多數程式設計師很少注意它。這容易理解 ― 我們中大多數很少需要使用非整數型別。除了科學計算和偶爾的計時測試或基準測試程式,其它情況下幾乎都用不著它。同樣,大多數開發人員也容易忽略 java.math.BigDecimal
所提供的任意精度的小數 ― 大多數應用程式不使用它們。然而,在以整數為主的程式中有時確實會出人意料地需要表示非整型資料。例如,JDBC 使用 BigDecimal
作為 SQL DECIMAL
列的首選互換格式。
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 的一些屬性。
表示式 | 結果 |
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。
這個表示式…… | 不一定等於…… | 當…… |
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)); } |
一些非整數值(如幾美元和幾美分這樣的小數)需要很精確。浮點數不是精確值,所以使用它們會導致舍入誤差。因此,使用浮點數來試圖表示象貨幣量這樣的精確數量不是一個好的想法。使用浮點數來進行美元和美分計算會得到災難性的後果。浮點數最好用來表示象測量值這類數值,這類值從一開始就不怎麼精確。
|
|
從 JDK 1.3 起,Java 開發人員就有了另一種數值表示法來表示非整數: BigDecimal
。 BigDecimal
是標準的類,在編譯器中不需要特殊支援,它可以表示任意精度的小數,並對它們進行計算。在內部,可以用任意精度任何範圍的值和一個換算因子來表示 BigDecimal
,換算因子表示左移小數點多少位,從而得到所期望範圍內的值。因此,用 BigDecimal
表示的數的形式為 unscaledValue*10 -scale
。
用於加、減、乘和除的方法給 BigDecimal
值提供了算術運算。由於 BigDecimal
物件是不可變的,這些方法中的每一個都會產生新的 BigDecimal
物件。因此,因為建立物件的開銷, BigDecimal
不適合於大量的數學計算,但設計它的目的是用來精確地表示小數。如果您正在尋找一種能精確表示如貨幣量這樣的數值,則 BigDecimal
可以很好地勝任該任務。
如浮點型別一樣, BigDecimal
也有一些令人奇怪的行為。尤其在使用 equals()
方法來檢測數值之間是否相等時要小心。 equals()
方法認為,兩個表示同一個數但換算值不同(例如, 100.00
和 100.000
)的 BigDecimal
值是不相等的。然而, compareTo()
方法會認為這兩個數是相等的,所以在從數值上比較兩個 BigDecimal
值時,應該使用 compareTo()
而不是 equals()
。
另外還有一些情形,任意精度的小數運算仍不能表示精確結果。例如, 1
除以 9
會產生無限迴圈的小數 .111111...
。出於這個原因,在進行除法運算時, BigDecimal
可以讓您顯式地控制舍入。 movePointLeft()
方法支援 10 的冪次方的精確除法。
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
,有幾個可用的建構函式。其中一個建構函式以雙精度浮點數作為輸入,另一個以整數和換算因子作為輸入,還有一個以小數的 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
。
相關文章
- Java浮點數計算Java
- Java中浮點數的坑Java
- 【Java】浮點數相等性比較Java
- 浮點數
- Java如何正確比較浮點數Java
- Java經典例項:比較浮點數Java
- 浮點數的理解
- 【求教:如何解決 java 浮點數精度問題】Java
- 浮點數小知識點
- 淺談浮點數(一)
- 轉換成浮點數
- [譯]浮點數的危害
- JAVA 字元轉浮點型Java字元
- 大數相乘(浮點數)實現
- java學習要點Java
- 組合語言學習筆記(十二)-浮點指令組合語言筆記
- java實戰1——浮點數轉人民幣讀法Java
- JS中如何理解浮點數?JS
- python處理浮點數Python
- iOS浮點數精度問題iOS
- JavaScript 浮點數陷阱及解法JavaScript
- 浮點數表示及其實現.
- 我的IEEE浮點數工具
- 補碼、反碼、浮點數
- JavaScript浮點數保留兩位小數JavaScript
- Java 浮點到字串轉換Java字串
- MySQL 有意思的浮點數和定點數MySql
- Java知識點學習Java
- 在Java中實現浮點數的精確計算 (轉)Java
- [Java] 浮點數的精度丟失問題與精度控制方法Java
- JS中浮點數精度問題JS
- js浮點數丟失問題JS
- 深入理解浮點數的表示
- java浮點型別案例介紹Java型別
- KLC 數點學習筆記筆記
- 【java學習】java知識點總結Java
- 如何判斷字串是否為合法數值、浮點、科學計數等格式字串
- java知識點學習圖Java