Java 理论与实践:您的小数点到哪里去了?(转载)
首席顧問,Quiotix Corp
2003 年 4 月
許多程序員在其整個開發(fā)生涯中都不曾使用定點或浮點數(shù),可能的例外是,偶爾在計時測試或基準(zhǔn)測試程序中會用到。Java 語言和類庫支持兩類非整數(shù)類型 — IEEE 754 浮點(float 和 double,包裝類(wrapper class)為 Float 和 Double),以及任意精度的小數(shù)(java.math.BigDecimal)。在本月的 Java 理論和實踐中,Brian Goetz 探討了在 Java 程序中使用非整數(shù)類型時一些常碰到的陷阱和“gotcha”。請在本文的論壇上提出您對本文的想法,以饗筆者和其他讀者。(您也可以單擊本文頂部或底部的討論來訪問論壇)。
雖然幾乎每種處理器和編程語言都支持浮點運(yùn)算,但大多數(shù)程序員很少注意它。這容易理解 — 我們中大多數(shù)很少需要使用非整數(shù)類型。除了科學(xué)計算和偶爾的計時測試或基準(zhǔn)測試程序,其它情況下幾乎都用不著它。同樣,大多數(shù)開發(fā)人員也容易忽略 java.math.BigDecimal 所提供的任意精度的小數(shù) — 大多數(shù)應(yīng)用程序不使用它們。然而,在以整數(shù)為主的程序中有時確實會出人意料地需要表示非整型數(shù)據(jù)。例如,JDBC 使用 BigDecimal 作為 SQL DECIMAL 列的首選互換格式。
IEEE 浮點
Java 語言支持兩種基本的浮點類型:float 和 double,以及與它們對應(yīng)的包裝類 Float 和 Double。它們都依據(jù) IEEE 754 標(biāo)準(zhǔn),該標(biāo)準(zhǔn)為 32 位浮點和 64 位雙精度浮點二進(jìn)制小數(shù)定義了二進(jìn)制標(biāo)準(zhǔn)。
IEEE 754 用科學(xué)記數(shù)法以底數(shù)為 2 的小數(shù)來表示浮點數(shù)。IEEE 浮點數(shù)用 1 位表示數(shù)字的符號,用 8 位來表示指數(shù),用 23 位來表示尾數(shù),即小數(shù)部分。作為有符號整數(shù)的指數(shù)可以有正負(fù)之分。小數(shù)部分用二進(jìn)制(底數(shù) 2)小數(shù)來表示,這意味著最高位對應(yīng)著值 ?(2-1),第二位對應(yīng)著 ?(2-2),依此類推。對于雙精度浮點數(shù),用 11 位表示指數(shù),52 位表示尾數(shù)。IEEE 浮點值的格式如圖 1 所示。
圖 1. IEEE 754 浮點數(shù)的格式
因為用科學(xué)記數(shù)法可以有多種方式來表示給定數(shù)字,所以要規(guī)范化浮點數(shù),以便用底數(shù)為 2 并且小數(shù)點左邊為 1 的小數(shù)來表示,按照需要調(diào)節(jié)指數(shù)就可以得到所需的數(shù)字。所以,例如,數(shù) 1.25 可以表示為尾數(shù)為 1.01,指數(shù)為 0:
(-1)0*1.012*20
數(shù) 10.0 可以表示為尾數(shù)為 1.01,指數(shù)為 3:
(-1)0*1.012*23
特殊數(shù)字
除了編碼所允許的值的標(biāo)準(zhǔn)范圍(對于 float,從 1.4e-45 到 3.4028235e+38),還有一些表示無窮大、負(fù)無窮大、-0 和 NaN(它代表“不是一個數(shù)字”)的特殊值。這些值的存在是為了在出現(xiàn)錯誤條件(譬如算術(shù)溢出,給負(fù)數(shù)開平方根,除以 0 等)下,可以用浮點值集合中的數(shù)字來表示所產(chǎn)生的結(jié)果。
這些特殊的數(shù)字有一些不尋常的特征。例如,0 和 -0 是不同值,但在比較它們是否相等時,被認(rèn)為是相等的。用一個非零數(shù)去除以無窮大的數(shù),結(jié)果等于 0。特殊數(shù)字 NaN 是無序的;使用 ==、< 和 > 運(yùn)算符將 NaN 與其它浮點值比較時,結(jié)果為 false。如果 f 為 NaN,則即使 (f == f) 也會得到 false。如果想將浮點值與 NaN 進(jìn)行比較,則使用 Float.isNaN() 方法。表 1 顯示了無窮大和 NaN 的一些屬性。
表 1. 特殊浮點值的屬性
| 表達(dá)式 | 結(jié)果 |
| Math.sqrt(-1.0) | -> NaN |
| 0.0 / 0.0 | -> NaN |
| 1.0 / 0.0 | -> 無窮大 |
| -1.0 / 0.0 | -> 負(fù)無窮大 |
| 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 的規(guī)則是不同的。對于 float 值,比較兩個 NaN 值是否相等將會得到 false,而使用 Float.equals() 來比較兩個 NaN Float 對象會得到 true。造成這種現(xiàn)象的原因是,如果不這樣的話,就不可能將 NaN Float 對象用作 HashMap 中的鍵。類似的,雖然 0 和 -0 在表示為浮點值時,被認(rèn)為是相等的,但使用 Float.compareTo() 來比較作為 Float 對象的 0 和 -0 時,會顯示 -0 小于 0。
浮點中的危險
由于無窮大、NaN 和 0 的特殊行為,當(dāng)應(yīng)用浮點數(shù)時,可能看似無害的轉(zhuǎn)換和優(yōu)化實際上是不正確的。例如,雖然好象 0.0-f 很明顯等于 -f,但當(dāng) f 為 0 時,這是不正確的。還有其它類似的 gotcha,表 2 顯示了其中一些 gotcha。
表 2. 無效的浮點假定
| 這個表達(dá)式…… | 不一定等于…… | 當(dāng)…… |
| 0.0 - f | -f | f 為 0 |
| f < g | ! (f >= g) | f 或 g 為 NaN |
| f == f | true | f 為 NaN |
| f + g - g | f | g 為無窮大或 NaN |
舍入誤差
浮點運(yùn)算很少是精確的。雖然一些數(shù)字(譬如 0.5)可以精確地表示為二進(jìn)制(底數(shù) 2)小數(shù)(因為 0.5 等于 2-1),但其它一些數(shù)字(譬如 0.1)就不能精確的表示。因此,浮點運(yùn)算可能導(dǎo)致舍入誤差,產(chǎn)生的結(jié)果接近 — 但不等于 — 您可能希望的結(jié)果。例如,下面這個簡單的計算將得到 2.600000000000001,而不是 2.6:
| double s=0; for (int i=0; i<26; i++) s += 0.1; System.out.println(s); |
類似的,.1*26 相乘所產(chǎn)生的結(jié)果不等于 .1 自身加 26 次所得到的結(jié)果。當(dāng)將浮點數(shù)強(qiáng)制轉(zhuǎn)換成整數(shù)時,產(chǎn)生的舍入誤差甚至更嚴(yán)重,因為強(qiáng)制轉(zhuǎn)換成整數(shù)類型會舍棄非整數(shù)部分,甚至對于那些“看上去似乎”應(yīng)該得到整數(shù)值的計算,也存在此類問題。例如,下面這些語句:
| double d = 29.0 * 0.01; System.out.println(d); System.out.println((int) (d * 100)); |
將得到以下輸出:
| 0.29 28 |
這可能不是您起初所期望的。
浮點數(shù)比較指南
由于存在 NaN 的不尋常比較行為和在幾乎所有浮點計算中都不可避免地會出現(xiàn)舍入誤差,解釋浮點值的比較運(yùn)算符的結(jié)果比較麻煩。
最好完全避免使用浮點數(shù)比較。當(dāng)然,這并不總是可能的,但您應(yīng)該意識到要限制浮點數(shù)比較。如果必須比較浮點數(shù)來看它們是否相等,則應(yīng)該將它們差的絕 對值同一些預(yù)先選定的小正數(shù)進(jìn)行比較,這樣您所做的就是測試它們是否“足夠接近”。(如果不知道基本的計算范圍,可以使用測試“abs(a/b - 1) < epsilon”,這種方法比簡單地比較兩者之差要更準(zhǔn)確)。甚至測試看一個值是比零大還是比零小也存在危險 —“以為”會生成比零略大值的計算事實上可能由于積累的舍入誤差會生成略微比零小的數(shù)字。
NaN 的無序性質(zhì)使得在比較浮點數(shù)時更容易發(fā)生錯誤。當(dāng)比較浮點數(shù)時,圍繞無窮大和 NaN 問題,一種避免 gotcha 的經(jīng)驗法則是顯式地測試值的有效性,而不是試圖排除無效值。在清單 1 中,有兩個可能的用于特性的 setter 的實現(xiàn),該特性只能接受非負(fù)數(shù)值。第一個實現(xiàn)會接受 NaN,第二個不會。第二種形式比較好,因為它顯式地檢測了您認(rèn)為有效的值的范圍。
清單 1. 需要非負(fù)浮點值的較好辦法和較差辦法| // 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)); } |
不要用浮點值表示精確值
一些非整數(shù)值(如幾美元和幾美分這樣的小數(shù))需要很精確。浮點數(shù)不是精確值,所以使用它們會導(dǎo)致舍入誤差。因此,使用浮點數(shù)來試圖表示象貨幣量這樣的精確 數(shù)量不是一個好的想法。使用浮點數(shù)來進(jìn)行美元和美分計算會得到災(zāi)難性的后果。浮點數(shù)最好用來表示象測量值這類數(shù)值,這類值從一開始就不怎么精確。
用于較小數(shù)的 BigDecimal
從 JDK 1.3 起,Java 開發(fā)人員就有了另一種數(shù)值表示法來表示非整數(shù):BigDecimal。BigDecimal 是標(biāo)準(zhǔn)的類,在編譯器中不需要特殊支持,它可以表示任意精度的小數(shù),并對它們進(jìn)行計算。在內(nèi)部,可以用任意精度任何范圍的值和一個換算因子來表示 BigDecimal,換算因子表示左移小數(shù)點多少位,從而得到所期望范圍內(nèi)的值。因此,用 BigDecimal 表示的數(shù)的形式為 unscaledValue*10-scale。
用于加、減、乘和除的方法給 BigDecimal 值提供了算術(shù)運(yùn)算。由于 BigDecimal 對象是不可變的,這些方法中的每一個都會產(chǎn)生新的 BigDecimal 對象。因此,因為創(chuàng)建對象的開銷,BigDecimal 不適合于大量的數(shù)學(xué)計算,但設(shè)計它的目的是用來精確地表示小數(shù)。如果您正在尋找一種能精確表示如貨幣量這樣的數(shù)值,則 BigDecimal 可以很好地勝任該任務(wù)。
所有的 equals 方法都不能真正測試相等
如浮點類型一樣,BigDecimal 也有一些令人奇怪的行為。尤其在使用 equals() 方法來檢測數(shù)值之間是否相等時要小心。equals() 方法認(rèn)為,兩個表示同一個數(shù)但換算值不同(例如,100.00 和 100.000)的 BigDecimal 值是不相等的。然而,compareTo() 方法會認(rèn)為這兩個數(shù)是相等的,所以在從數(shù)值上比較兩個 BigDecimal 值時,應(yīng)該使用 compareTo() 而不是 equals()。
另外還有一些情形,任意精度的小數(shù)運(yùn)算仍不能表示精確結(jié)果。例如,1 除以 9 會產(chǎn)生無限循環(huán)的小數(shù) .111111...。出于這個原因,在進(jìn)行除法運(yùn)算時,BigDecimal 可以讓您顯式地控制舍入。movePointLeft() 方法支持 10 的冪次方的精確除法。
使用 BigDecimal 作為互換類型
SQL-92 包括 DECIMAL 數(shù)據(jù)類型,它是用于表示定點小數(shù)的精確數(shù)字類型,它可以對小數(shù)進(jìn)行基本的算術(shù)運(yùn)算。一些 SQL 語言喜歡稱此類型為 NUMERIC 類型,其它一些 SQL 語言則引入了 MONEY 數(shù)據(jù)類型,MONEY 數(shù)據(jù)類型被定義為小數(shù)點右側(cè)帶有兩位的小數(shù)。
如果希望將數(shù)字存儲到數(shù)據(jù)庫中的 DECIMAL 字段,或從 DECIMAL 字段檢索值,則如何確保精確地轉(zhuǎn)換該數(shù)字?您可能不希望使用由 JDBC PreparedStatement 和 ResultSet 類所提供的 setFloat() 和 getFloat() 方法,因為浮點數(shù)與小數(shù)之間的轉(zhuǎn)換可能會喪失精確性。相反,請使用 PreparedStatement 和 ResultSet 的 setBigDecimal() 及 getBigDecimal() 方法。
類似的,象 Castor 這樣的 XML 數(shù)據(jù)綁定工具使用 BigDecimal 會生成小數(shù)值屬性和元素(在 XSD 模式中支持這種基本數(shù)據(jù)類型)的 getter 和 setter。
構(gòu)造 BigDecimal 數(shù)
對于 BigDecimal,有幾個可用的構(gòu)造函數(shù)。其中一個構(gòu)造函數(shù)以雙精度浮點數(shù)作為輸入,另一個以整數(shù)和換算因子作為輸入,還有一個以小數(shù)的 String 表示作為輸入。要小心使用 BigDecimal(double) 構(gòu)造函數(shù),因為如果不了解它,會在計算過程中產(chǎn)生舍入誤差。請使用基于整數(shù)或 String 的構(gòu)造函數(shù)。
如果使用 BigDecimal(double) 構(gòu)造函數(shù)不恰當(dāng),在傳遞給 JDBC setBigDecimal() 方法時,會造成似乎很奇怪的 JDBC 驅(qū)動程序中的異常。例如,考慮以下 JDBC 代碼,該代碼希望將數(shù)字 0.01 存儲到小數(shù)字段:
| PreparedStatement ps = connection.prepareStatement("INSERT INTO Foo SET name=?, value=?"); ps.setString(1, "penny"); ps.setBigDecimal(2, new BigDecimal(0.01)); ps.executeUpdate(); |
在執(zhí)行這段似乎無害的代碼時會拋出一些令人迷惑不解的異常(這取決于具體的 JDBC 驅(qū)動程序),因為 0.01 的雙精度近似值會導(dǎo)致大的換算值,這可能會使 JDBC 驅(qū)動程序或數(shù)據(jù)庫感到迷惑。JDBC 驅(qū)動程序會產(chǎn)生異常,但可能不會說明代碼實際上錯在哪里,除非意識到二進(jìn)制浮點數(shù)的局限性。相反,使用 BigDecimal("0.01") 或 BigDecimal(1, 2) 構(gòu)造 BigDecimal 來避免這類問題,因為這兩種方法都可以精確地表示小數(shù)。
結(jié)束語
在 Java 程序中使用浮點數(shù)和小數(shù)充滿著陷阱。浮點數(shù)和小數(shù)不象整數(shù)一樣“循規(guī)蹈矩”,不能假定浮點計算一定產(chǎn)生整型或精確的結(jié)果,雖然它們的確“應(yīng)該”那樣做。最 好將浮點運(yùn)算保留用作計算本來就不精確的數(shù)值,譬如測量。如果需要表示定點數(shù)(譬如,幾美元和幾美分),則使用 BigDecimal。
轉(zhuǎn)載于:https://www.cnblogs.com/Ella_xujiaona/archive/2005/03/28/127065.html
總結(jié)
以上是生活随笔為你收集整理的Java 理论与实践:您的小数点到哪里去了?(转载)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 制作漂亮的电子杂志
- 下一篇: 求最长回文串-从动态规划到马拉车之路(上