- 相關(guān)推薦
Java程序中使用浮點(diǎn)數(shù)和小數(shù)的技巧
許多程序員在其整個(gè)開發(fā)生涯中都不曾使用定點(diǎn)或浮點(diǎn)數(shù),可能的例外是,偶爾在計(jì)時(shí)測(cè)試或基準(zhǔn)測(cè)試程序中會(huì)用到。下面小編準(zhǔn)備了關(guān)于Java程序中使用浮點(diǎn)數(shù)和小數(shù)的技巧,歡迎大家參考!
雖然幾乎每種處理器和編程語言都支持浮點(diǎn)運(yùn)算,但大多數(shù)程序員很少注意它。這容易理解 ― 我們中大多數(shù)很少需要使用非整數(shù)類型。除了科學(xué)計(jì)算和偶爾的計(jì)時(shí)測(cè)試或基準(zhǔn)測(cè)試程序,其它情況下幾乎都用不著它。同樣,大多數(shù)開發(fā)人員也容易忽略 java.math.BigDecimal 所提供的任意精度的小數(shù) ― 大多數(shù)應(yīng)用程序不使用它們。然而,在以整數(shù)為主的程序中有時(shí)確實(shí)會(huì)出人意料地需要表示非整型數(shù)據(jù)。例如,JDBC 使用 BigDecimal 作為 SQL DECIMAL 列的首選互換格式。
IEEE 浮點(diǎn)
Java 語言支持兩種基本的浮點(diǎn)類型: float 和 double ,以及與它們對(duì)應(yīng)的包裝類 Float 和 Double .它們都依據(jù) IEEE 754 標(biāo)準(zhǔn),該標(biāo)準(zhǔn)為 32 位浮點(diǎn)和 64 位雙精度浮點(diǎn)二進(jìn)制小數(shù)定義了二進(jìn)制標(biāo)準(zhǔn)。
IEEE 754 用科學(xué)記數(shù)法以底數(shù)為 2 的小數(shù)來表示浮點(diǎn)數(shù)。IEEE 浮點(diǎn)數(shù)用 1 位表示數(shù)字的符號(hào),用 8 位來表示指數(shù),用 23 位來表示尾數(shù),即小數(shù)部分。作為有符號(hào)整數(shù)的指數(shù)可以有正負(fù)之分。小數(shù)部分用二進(jìn)制(底數(shù) 2)小數(shù)來表示,這意味著最高位對(duì)應(yīng)著值 ?(2 -1),第二位對(duì)應(yīng)著 ?(2 -2),依此類推。對(duì)于雙精度浮點(diǎn)數(shù),用 11 位表示指數(shù),52 位表示尾數(shù)。IEEE 浮點(diǎn)值的格式如圖 1 所示。
因?yàn)橛每茖W(xué)記數(shù)法可以有多種方式來表示給定數(shù)字,所以要規(guī)范化浮點(diǎn)數(shù),以便用底數(shù)為 2 并且小數(shù)點(diǎn)左邊為 1 的小數(shù)來表示,按照需要調(diào)節(jié)指數(shù)就可以得到所需的數(shù)字。所以,例如,數(shù) 1.25 可以表示為尾數(shù)為 1.01,指數(shù)為 0: (-1) 0*1.01 2*2 0
數(shù) 10.0 可以表示為尾數(shù)為 1.01,指數(shù)為 3: (-1) 0*1.01 2*2 3
特殊數(shù)字
除了編碼所允許的值的標(biāo)準(zhǔn)范圍(對(duì)于 float ,從 1.4e-45 到 3.4028235e+38),還有一些表示無窮大、負(fù)無窮大、 -0 和 NaN(它代表“不是一個(gè)數(shù)字”)的特殊值。這些值的存在是為了在出現(xiàn)錯(cuò)誤條件(譬如算術(shù)溢出,給負(fù)數(shù)開平方根,除以 0 等)下,可以用浮點(diǎn)值集合中的數(shù)字來表示所產(chǎn)生的結(jié)果。
這些特殊的數(shù)字有一些不尋常的特征。例如, 0 和 -0 是不同值,但在比較它們是否相等時(shí),被認(rèn)為是相等的。用一個(gè)非零數(shù)去除以無窮大的數(shù),結(jié)果等于 0 .特殊數(shù)字 NaN 是無序的;使用 == 、 < 和 > 運(yùn)算符將 NaN 與其它浮點(diǎn)值比較時(shí),結(jié)果為 false .如果 f 為 NaN,則即使 (f == f) 也會(huì)得到 false .如果想將浮點(diǎn)值與 NaN 進(jìn)行比較,則使用 Float.isNaN() 方法。表 1 顯示了無窮大和 NaN 的一些屬性。
表 1. 特殊浮點(diǎn)值的屬性
表達(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
基本浮點(diǎn)類型和包裝類浮點(diǎn)有不同的比較行為
使事情更糟的是,在基本 float 類型和包裝類 Float 之間,用于比較 NaN 和 -0 的規(guī)則是不同的。對(duì)于 float 值,比較兩個(gè) NaN 值是否相等將會(huì)得到 false ,而使用 Float.equals() 來比較兩個(gè) NaN Float 對(duì)象會(huì)得到 true .造成這種現(xiàn)象的原因是,如果不這樣的話,就不可能將 NaN Float 對(duì)象用作 HashMap 中的鍵。類似的,雖然 0 和 -0 在表示為浮點(diǎn)值時(shí),被認(rèn)為是相等的,但使用 Float.compareTo() 來比較作為 Float 對(duì)象的 0 和 -0 時(shí),會(huì)顯示 -0 小于 0 .
浮點(diǎn)中的危險(xiǎn)
由于無窮大、NaN 和 0 的特殊行為,當(dāng)應(yīng)用浮點(diǎn)數(shù)時(shí),可能看似無害的轉(zhuǎn)換和優(yōu)化實(shí)際上是不正確的。例如,雖然好象 0.0-f 很明顯等于 -f ,但當(dāng) f 為 0 時(shí),這是不正確的。還有其它類似的 gotcha,表 2 顯示了其中一些 gotcha.
表 2. 無效的浮點(diǎn)假定
這個(gè)表達(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舍入誤差
浮點(diǎn)運(yùn)算很少是精確的。雖然一些數(shù)字(譬如 0.5 )可以精確地表示為二進(jìn)制(底數(shù) 2)小數(shù)(因?yàn)?0.5 等于 2 -1),但其它一些數(shù)字(譬如 0.1 )就不能精確的表示。因此,浮點(diǎn)運(yùn)算可能導(dǎo)致舍入誤差,產(chǎn)生的結(jié)果接近 ― 但不等于 ― 您可能希望的結(jié)果。例如,下面這個(gè)簡(jiǎn)單的計(jì)算將得到 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)將浮點(diǎn)數(shù)強(qiáng)制轉(zhuǎn)換成整數(shù)時(shí),產(chǎn)生的舍入誤差甚至更嚴(yán)重,因?yàn)閺?qiáng)制轉(zhuǎn)換成整數(shù)類型會(huì)舍棄非整數(shù)部分,甚至對(duì)于那些“看上去似乎”應(yīng)該得到整數(shù)值的計(jì)算,也存在此類問題。例如,下面這些語句:
double d = 29.0 * 0.01; System.out.println(d); System.out.println((int) (d * 100));
將得到以下輸出:
0.29 28
這可能不是您起初所期望的。
浮點(diǎn)數(shù)比較指南
由于存在 NaN 的不尋常比較行為和在幾乎所有浮點(diǎn)計(jì)算中都不可避免地會(huì)出現(xiàn)舍入誤差,解釋浮點(diǎn)值的比較運(yùn)算符的結(jié)果比較麻煩。
最好完全避免使用浮點(diǎn)數(shù)比較。當(dāng)然,這并不總是可能的,但您應(yīng)該意識(shí)到要限制浮點(diǎn)數(shù)比較。如果必須比較浮點(diǎn)數(shù)來看它們是否相等,則應(yīng)該將它們差的絕對(duì)值同一些預(yù)先選定的小正數(shù)進(jìn)行比較,這樣您所做的就是測(cè)試它們是否“足夠接近”。(如果不知道基本的計(jì)算范圍,可以使用測(cè)試 “abs(a/b - 1) < epsilon”,這種方法比簡(jiǎn)單地比較兩者之差要更準(zhǔn)確)。甚至測(cè)試看一個(gè)值是比零大還是比零小也存在危險(xiǎn) ―“以為”會(huì)生成比零略大值的計(jì)算事實(shí)上可能由于積累的舍入誤差會(huì)生成略微比零小的數(shù)字。
NaN 的無序性質(zhì)使得在比較浮點(diǎn)數(shù)時(shí)更容易發(fā)生錯(cuò)誤。當(dāng)比較浮點(diǎn)數(shù)時(shí),圍繞無窮大和 NaN 問題,一種避免 gotcha 的經(jīng)驗(yàn)法則是顯式地測(cè)試值的有效性,而不是試圖排除無效值。在清單 1 中,有兩個(gè)可能的用于特性的 setter 的實(shí)現(xiàn),該特性只能接受非負(fù)數(shù)值。第一個(gè)實(shí)現(xiàn)會(huì)接受 NaN,第二個(gè)不會(huì)。第二種形式比較好,因?yàn)樗@式地檢測(cè)了您認(rèn)為有效的值的范圍。
清單 1. 需要非負(fù)浮點(diǎn)值的較好辦法和較差辦法
// 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)); }不要用浮點(diǎn)值表示精確值
一些非整數(shù)值(如幾美元和幾美分這樣的小數(shù))需要很精確。浮點(diǎn)數(shù)不是精確值,所以使用它們會(huì)導(dǎo)致舍入誤差。因此,使用浮點(diǎn)數(shù)來試圖表示象貨幣量這樣的精確數(shù)量不是一個(gè)好的想法。使用浮點(diǎn)數(shù)來進(jìn)行美元和美分計(jì)算會(huì)得到災(zāi)難性的后果。浮點(diǎn)數(shù)最好用來表示象測(cè)量值這類數(shù)值,這類值從一開始就不怎么精確。
用于較小數(shù)的 BigDecimal
從 JDK 1.3 起,Java 開發(fā)人員就有了另一種數(shù)值表示法來表示非整數(shù): BigDecimal . BigDecimal 是標(biāo)準(zhǔn)的類,在編譯器中不需要特殊支持,它可以表示任意精度的小數(shù),并對(duì)它們進(jìn)行計(jì)算。在內(nèi)部,可以用任意精度任何范圍的值和一個(gè)換算因子來表示 BigDecimal ,換算因子表示左移小數(shù)點(diǎn)多少位,從而得到所期望范圍內(nèi)的值。因此,用 BigDecimal 表示的數(shù)的形式為 unscaledValue*10 -scale .
用于加、減、乘和除的方法給 BigDecimal 值提供了算術(shù)運(yùn)算。由于 BigDecimal 對(duì)象是不可變的,這些方法中的每一個(gè)都會(huì)產(chǎn)生新的 BigDecimal 對(duì)象。因此,因?yàn)閯?chuàng)建對(duì)象的開銷, BigDecimal 不適合于大量的數(shù)學(xué)計(jì)算,但設(shè)計(jì)它的目的是用來精確地表示小數(shù)。如果您正在尋找一種能精確表示如貨幣量這樣的數(shù)值,則 BigDecimal 可以很好地勝任該任務(wù)。
所有的 equals 方法都不能真正測(cè)試相等
如浮點(diǎn)類型一樣, BigDecimal 也有一些令人奇怪的行為。尤其在使用 equals() 方法來檢測(cè)數(shù)值之間是否相等時(shí)要小心。 equals() 方法認(rèn)為,兩個(gè)表示同一個(gè)數(shù)但換算值不同(例如, 100.00 和 100.000 )的 BigDecimal 值是不相等的。然而, compareTo() 方法會(huì)認(rèn)為這兩個(gè)數(shù)是相等的,所以在從數(shù)值上比較兩個(gè) BigDecimal 值時(shí),應(yīng)該使用 compareTo() 而不是 equals() .
另外還有一些情形,任意精度的小數(shù)運(yùn)算仍不能表示精確結(jié)果。例如, 1 除以 9 會(huì)產(chǎn)生無限循環(huán)的小數(shù) .111111…… .出于這個(gè)原因,在進(jìn)行除法運(yùn)算時(shí), BigDecimal 可以讓您顯式地控制舍入。 movePointLeft() 方法支持 10 的冪次方的精確除法。
使用 BigDecimal 作為互換類型
SQL-92 包括 DECIMAL 數(shù)據(jù)類型,它是用于表示定點(diǎn)小數(shù)的精確數(shù)字類型,它可以對(duì)小數(shù)進(jìn)行基本的算術(shù)運(yùn)算。一些 SQL 語言喜歡稱此類型為 NUMERIC 類型,其它一些 SQL 語言則引入了 MONEY 數(shù)據(jù)類型,MONEY 數(shù)據(jù)類型被定義為小數(shù)點(diǎn)右側(cè)帶有兩位的小數(shù)。
如果希望將數(shù)字存儲(chǔ)到數(shù)據(jù)庫中的 DECIMAL 字段,或從 DECIMAL 字段檢索值,則如何確保精確地轉(zhuǎn)換該數(shù)字?您可能不希望使用由 JDBC PreparedStatement 和 ResultSet 類所提供的 setFloat() 和 getFloat() 方法,因?yàn)楦↑c(diǎn)數(shù)與小數(shù)之間的轉(zhuǎn)換可能會(huì)喪失精確性。相反,請(qǐng)使用 PreparedStatement 和 ResultSet 的 setBigDecimal() 及 getBigDecimal() 方法。
對(duì)于 BigDecimal ,有幾個(gè)可用的構(gòu)造函數(shù)。其中一個(gè)構(gòu)造函數(shù)以雙精度浮點(diǎn)數(shù)作為輸入,另一個(gè)以整數(shù)和換算因子作為輸入,還有一個(gè)以小數(shù)的 String 表示作為輸入。要小心使用 BigDecimal(double) 構(gòu)造函數(shù),因?yàn)槿绻涣私馑,?huì)在計(jì)算過程中產(chǎn)生舍入誤差。請(qǐng)使用基于整數(shù)或 String 的構(gòu)造函數(shù)。
構(gòu)造 BigDecimal 數(shù)
對(duì)于 BigDecimal ,有幾個(gè)可用的構(gòu)造函數(shù)。其中一個(gè)構(gòu)造函數(shù)以雙精度浮點(diǎn)數(shù)作為輸入,另一個(gè)以整數(shù)和換算因子作為輸入,還有一個(gè)以小數(shù)的 String 表示作為輸入。要小心使用 BigDecimal(double) 構(gòu)造函數(shù),因?yàn)槿绻涣私馑,?huì)在計(jì)算過程中產(chǎn)生舍入誤差。請(qǐng)使用基于整數(shù)或 String 的構(gòu)造函數(shù)。
如果使用 BigDecimal(double) 構(gòu)造函數(shù)不恰當(dāng),在傳遞給 JDBC setBigDecimal() 方法時(shí),會(huì)造成似乎很奇怪的 JDBC 驅(qū)動(dòng)程序中的異常。例如,考慮以下 JDBC 代碼,該代碼希望將數(shù)字 0.01 存儲(chǔ)到小數(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í)行這段似乎無害的代碼時(shí)會(huì)拋出一些令人迷惑不解的異常(這取決于具體的 JDBC 驅(qū)動(dòng)程序),因?yàn)?0.01 的雙精度近似值會(huì)導(dǎo)致大的換算值,這可能會(huì)使 JDBC 驅(qū)動(dòng)程序或數(shù)據(jù)庫感到迷惑。JDBC 驅(qū)動(dòng)程序會(huì)產(chǎn)生異常,但可能不會(huì)說明代碼實(shí)際上錯(cuò)在哪里,除非意識(shí)到二進(jìn)制浮點(diǎn)數(shù)的局限性。相反,使用 BigDecimal("0.01") 或 BigDecimal(1, 2) 構(gòu)造 BigDecimal 來避免這類問題,因?yàn)檫@兩種方法都可以精確地表示小數(shù)。
【Java程序中使用浮點(diǎn)數(shù)和小數(shù)的技巧】相關(guān)文章:
Java程序員使用大數(shù)據(jù)工具匯總07-07
java中File類的使用方法09-07
word 2010使用中的小技巧07-01
戶外睡袋的使用技巧和講究07-20
戶外背包的使用和選購技巧09-29
使用和維護(hù)硬盤的小技巧09-16