1. 程式人生 > >Java 中double和float型別計算丟失精度問題總結

Java 中double和float型別計算丟失精度問題總結

背景

問題發生在某天中午,當我訂單付完款後,不想要了就點選了全額退款,但是給我的提示確實 “您輸入的金額不正確”,我就納悶了,為什麼不能退?看了下程式碼,然後就發現了問題...

1、bigdecimal 轉換成小數計算有誤差

  • 真實專案中校驗退款金額是否超過訂單實付款金額程式碼如下截圖: 錯誤程式碼
  • 模擬以上的程式碼截圖如下: 模擬程式碼
  • float和double做四則運算誤差
    public static void main( String[] args ) {
    	 System.out.println(0.05+0.01);
         System.out.println(1.0-0.42);
         System.out.println(4.015*100);
         System.out.println(123.3/100);
    }
    
    //輸出
    0.060000000000000005
    0.5800000000000001
    401.49999999999994
    1.2329999999999999

  • 根源

為什麼會出現精度丟失,得從計算機說起,計算機並不能識別除了二進位制資料以外的任何資料。無論我們使用何種程式語言,在何種編譯環境下工作,都要先把源程式翻譯成二進位制的機器碼後才能被計算機識別。以上面提到的情況為例,我們源程式裡的2.4是十進位制的,計算機不能直接識別,要先編譯成二進位制。但問題來了,2.4的二進位制表示並非是精確的2.4,反而最為接近的二進位制表示是2.3999999999999999。原因在於浮點數由兩部分組成:指數和尾數,這點如果知道怎樣進行浮點數的二進位制與十進位制轉換,應該是不難理解的。如果在這個轉換的過程中,浮點數參與了計算,那麼轉換的過程就會變得不可預 知,並且變得不可逆。我們有理由相信,就是在這個過程中,發生了精度的丟失。而至於為什麼有些浮點計算會得到準確的結果,應該也是碰巧那個計算的二進位制與 十進位制之間能夠準確轉換。而當輸出單個浮點型資料的時候,可以正確輸出,如double d = 2.4;System.out.println(d); 輸出的是2.4,而不是2.3999999999999999。也就是說,不進行浮點計算的時候,在十進位制裡浮點數能正確顯示。這更印證了我以上的想法,即如果浮點數參與了計算,那麼浮點數二進位制與十進位制間的轉換過程就會變得不可預知,並且變得不可逆。

事實上,浮點數並不適合用於精確計算,而適合進行科學計算。這裡有一個小知識:既然float和double型用來表示帶有小數點的數,那為什麼我們不稱 它們為“小數”或者“實數”,要叫浮點數呢?因為這些數都以科學計數法的形式儲存。當一個數如50.534,轉換成科學計數法的形式為5.053e1,它 的小數點移動到了一個新的位置(即浮動了)。可見,浮點數本來就是用於科學計算的,用來進行精確計算實在太不合適了。

2、bigdecimal建構函式使用不當時,結果仍然異常

	public static void main(String[] args) {
		
		BigDecimal aa = new BigDecimal(0.31);
		BigDecimal bb = new BigDecimal(0.2);
		BigDecimal cc = new BigDecimal(0.11);
		
		
		System.out.println(aa.subtract(bb).doubleValue()); //轉換成double
		System.out.println(aa.subtract(bb));
		System.out.println(cc.compareTo(aa.subtract(bb))); 
		
	}
	
	//輸出
	0.10999999999999999
    0.109999999999999986677323704498121514916419982910156250
    1 
	

BigDecimal其中一個建構函式以雙精度浮點數作為輸入,然而得到的結果仍然不是我們想要的。要小心使用 BigDecimal(double)建構函式,因為如果不瞭解它,會在計算過程中產生舍入誤差。請使用基於整數或 String 的建構函式。

3、解決double和float精確計算有誤差的方法

在《Effective Java》這本書中也提到這個原則,float和double只能用來做科學計算或者是工程計算,在商業計算中我們要用java.math.BigDecimal。使用BigDecimal並且一定要用String來夠造。

BigDecimal用哪個建構函式?

BigDecimal(double val)

BigDecimal(String val)
上面的API簡要描述相當的明確,而且通常情況下,上面的那一個使用起來要方便一些。我們可能想都不想就用上了,會有什麼問題呢?等到出了問題的時候,才發現引數是double的構造方法的詳細說明中有這麼一段:

Note: the results of this constructor can be somewhat unpredictable. One might assume that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to .1000000000000000055511151231257827021181583404541015625. This is so because .1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the long value that is being passed in to the constructor is not exactly equal to .1, appearances nonwithstanding.

The (String) constructor, on the other hand, is perfectly predictable: new BigDecimal(".1") is exactly equal to .1, as one would expect. Therefore, it is generally recommended that the (String) constructor be used in preference to this one.

原來我們如果需要精確計算,非要用String來夠造BigDecimal不可!

程式碼示例:

    public static void main(String[] args) {
		
		BigDecimal aa = new BigDecimal("0.31");
		BigDecimal bb = new BigDecimal("0.2");
		BigDecimal cc = new BigDecimal("0.11");
		
		
		System.out.println(aa.subtract(bb).doubleValue()); 
		System.out.println(aa.subtract(bb));
		System.out.println(cc.compareTo(aa.subtract(bb))); 
		
	}
	
	//輸出
	0.11
    0.11
    0
	

4、bigdecimal比較數值大小的方法

如浮點型別一樣,BigDecimal 也有一些令人奇怪的行為。尤其在使用 equals() 方法來檢測數值之間是否相等時要小心。 equals() 方法認為,兩個表示同一個數但換算值不同(例如, 100.00 和 100.000 )的 BigDecimal 值是不相等的。然而, compareTo() 方法會認為這兩個數是相等的,所以在從數值上比較兩個 BigDecimal 值時,應該使用 compareTo() 而不是 equals() 。

另外還有一些情形,任意精度的小數運算仍不能表示精確結果。例如,1 除以 9 會產生無限迴圈的小數 .111111... 。出於這個原因,在進行除法運算時,BigDecimal 可以讓您顯式地控制舍入。 movePointLeft() 方法支援 10 的冪次方的精確除法。

    public static void main(String[] args) {
		
		BigDecimal aa = new BigDecimal("0.31");
		BigDecimal bb = new BigDecimal("0.2");
		BigDecimal cc = new BigDecimal("0.11");
		
		
		int r = cc.compareTo(aa.subtract(bb));
		
		if(r==0) //等於
        if(r==1) //大於
        if(r==-1) //小於
		
	}