標籤:
在大多數行業涉及到浮點數的計算的情境比較少,但是在金融、支付行業就比較多了,而且在這兩個行業一個小小的錯誤
可能將會給公司帶來極大的損失。
以前我們公司就出現了這樣的一個問題,當時使用的是double類型進行計算,導致計算出來的結果與實際的結果少了幾十
元錢。雖然數額不大,但是引起產品、技術的重視。通過查閱相關資料,終於知道了是因為float、double在對含有小數的數值進行
計算過程中的舍入產生了誤差。
在浮點運算中,浮點運算很少是精確的。雖然一些數字(譬如 0.5 )可以精確地表示為二進位(底數 2)小數(因為 0.5
等於 2 -1),但其它一些數字(譬如 0.1 )就不能精確的表示。因此,浮點運算可能導致舍入誤差,產生的結果接近但不等於你
可能希望的結果。比如:下面的代碼運行結果的實際值為100958.34,而不是100000。大家可以運行試一下。
public static void main(String[] args) {float f = 0.1f;float sum = 0;for( int i=0; i<1000000; i++){ sum += f;}System.out.println(sum);}
類似的,.1*26相乘所產生的結果不等於.1自身加26次所得到的結果。當將浮點數強制轉換成整數時,產生的舍入誤差甚至
更嚴重,因為強制轉換成整數類型會捨棄非整數部分,甚至對於那些“看上去似乎”應該得到整數值的計算,也存在此類問題。例如
下面這段代碼:
public static void main(String[] args) {double d = 29.0 * 0.01;System.out.println(d);System.out.println((int) (d * 100));}
運行結果:
0.2928
看到結果是不是非常差異,所以建議大家不要用float、double等浮點值表示精確值一些非整數值(如幾美元和幾美分這樣的
小數)需要很精確。浮點數不是精確值,所以使用它們會導致舍入誤差。因此,使用浮點數來試圖表示象貨幣量這樣的精確數量不是
一個好的想法。使用浮點數來進行美元和美分計算會得到災難性的後果。浮點數最好用來表示像測量值這類數值,這類值從一開始就
不怎麼精確。
那我們怎樣來解決這樣的問題呢?
JDK開發人員在很早就遇到了這個問題,並在JDK1.3起給我們提供了一種新的處理精確值的類BigDecimal,BigDecimal是標
準的類,在編譯器中不需要特殊支援,它可以表示任意精度的小數,並對它們進行計算。在內部,可以用任意精度任何範圍的值和一個
換算因子來表示 BigDecimal,換算因子表示左移小數點多少位,從而得到所期望範圍內的值BigDecimal 給我們提供了加、減、乘和除
等算術運算,由於BigDecimal是一個類,而且對象是不可變,不像float、double是基本變數,所以計算後的結果將放入一個新BigDec
imal對象中,所以使用BigDecimal會佔用很大的開銷,不適合大規模的數學計算。設計它的目的是用來精確地表示小數,所以我們可以
用它來表示貨幣和金額的計算。
BigDecimal 用法:
BigDecimal構造方法:
BigDecimal 一共有4個構造方法BigDecimal(int) 建立一個具有參數所指定整數值的對象。BigDecimal(double) 建立一個具有參數所指定雙精確度值的對象。BigDecimal(long) 建立一個具有參數所指定長整數值的對象。BigDecimal(String) 建立一個具有參數所指定以字串表示的數值的對象。
BigDecimal 的運算方式 不支援 + - * / 這類的運算 它有自己的運算方法:
BigDecimal add(BigDecimal augend) 加法運算BigDecimal subtract(BigDecimal subtrahend) 減法運算BigDecimal multiply(BigDecimal multiplicand) 乘法運算BigDecimal divide(BigDecimal divisor) 除法運算
我們現在來看以第一個float精度錯誤的例子,用BigDecimal計算:
public static void main(String[] args) {float f = 0.1f;float sum = 0;for( int i=0; i<1000000; i++){ sum += f;}System.out.println("float sum="+sum);BigDecimal b1 = new BigDecimal(Double.toString(0.01)); BigDecimal total = new BigDecimal(Double.toString(0)); for( int i=0; i<1000000; i++){total=total.add(b1);}System.out.println("BigDecimal total="+total);}
結果:
float sum=100958.34BigDecimal total=10000.00
我們從結果中可以看出使用float計算結果是有誤差的,而使用BigDecimal計算結果是正確的。我們再看以第二個double精度
錯誤的例子,用BigDecimal計算:
public static void main(String[] args) {double d = 29.0 * 0.01;System.out.println(d);/***double計算**/System.out.println(d * 100);/***BigDecimal計算**/BigDecimal b1 = new BigDecimal(Double.toString(d)); BigDecimal b2 = new BigDecimal(Double.toString(100)); System.out.println( b1.multiply(b2).doubleValue() );}
結果:
0.2928.99999999999999629.0
我們從結果中可以看出使用double計算結果是有誤差的,而使用BigDecimal計算結果是正確的
從上面兩個例子可以看出BigDecimal能夠很好處理浮點數計算精度問題,但是效能方面比float、double低很多,但是為了提高
計算的精確度,尤其是像處理金額,建議大家還是使用BigDecimal進行計算,避免造成損失。
結束語:
結束語是引用IBM文件庫裡面的一段話:"在Java程式中使用浮點數和小數充滿著陷阱。浮點數和小數不象整數一樣“循規蹈矩”,
不能假定浮點計算一定產生整型或精確的結果,雖然它們的確“應該”那樣做。最好將浮點運算保留用作計算本來就不精確的數值,譬如
測量。如果需要表示定點數(譬如,幾美元和幾美分),則使用 BigDecimal 。"。
註:本文部分內容參考 IBM developerWorks中國的文件庫
金融、支付行業的開發人員不得不知道的float、double計算誤差問題