金融、支付行業的開發者不得不知道的float、double計算誤差問題

在大多數行業涉及到浮點數的計算的場景比較少,但是在金融、支付行業就比較多了,而且在這兩個行業一個小小的錯誤

可能將會給公司帶來極大的損失。


以前我們公司就出現了這樣的一個問題,當時使用的是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.29
28

看到結果是不是非常差異,所以建議大家不要用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.34
BigDecimal 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.29
28.999999999999996
29.0

我們從結果中可以看出使用double計算結果是有誤差的,而使用BigDecimal計算結果是正確的


從上面兩個例子可以看出BigDecimal能夠很好處理浮點數計算精度問題,但是性能方面比float、double低很多,但是爲了提高
計算的精確度,尤其是像處理金額,建議大家還是使用BigDecimal進行計算,避免造成損失。

結束語:

結束語是引用IBM文檔庫裏面的一段話:"在Java程序中使用浮點數和小數充滿着陷阱。浮點數和小數不象整數一樣“循規蹈矩”,

不能假定浮點計算一定產生整型或精確的結果,雖然它們的確“應該”那樣做。最好將浮點運算保留用作計算本來就不精確的數值,譬如

測量。如果需要表示定點數(譬如,幾美元和幾美分),則使用 BigDecimal 。"。


注:本文部分內容參考 IBM developerWorks中國的文檔庫


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章