JAVA編程——論浮點運算損失精度問題

對這個問題相信都不陌生:

無論你使用的是什麼編程語言,在使用浮點型數據進行精確計算時,都有可能出現精度損失問題。

來看下面的例子:

// 這是一個利用浮點型數據進行精確計算時結果出錯的例子,使用Java編寫,有所省略。

<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre">	</span>Double a = (1.2 - 0.4) / 0.1;
<span style="white-space:pre">	</span>System.out.println(a);</span>

如果你認爲這個程序的輸出結果是“8”的話,那你就錯了。實際上,程序的輸出結果是“7.999999999999999”。

好,問題來了。到底是哪裏出了錯?

浮點型數據進行精確計算時,該類問題並不少見。我們姑且稱其爲“精度丟失”吧。大家可以試着改一下上面的程序,你會發現一些有趣的現象:

1、如果直接使用一個數字代替括號裏的表達式,如“0.8 / 0.1”或者“1.1 /0.1”,那麼似乎,注意只是似乎不會出現問題;

2、我們可能會做第二個測試,就是對比“0.8 / 1.1”和“(1.2 - 0.4) / 1.1”的結果,沒錯,我就是這樣做的。那麼你會發現,前者的結果是“0.7272727272727273”(四捨五入後的結果),而後者的結果是 “0.7272727272727272”(沒有進行四舍五入)。可以推測,經過一次計算後,精度丟失了;

3、很好,我覺得我們已經很接近真相了,但是接下來的第三個測試或許會讓你泄氣,我是這樣做的:對比“(2.4 - 0.1) /0.1”、“(1.2 - 0.1) / 0.1”以及“(1.1 - 0.1) / 0.1”的結果,第一個是“22.999999999999996”,第二個是“10.999999999999998”,第三個是“10.0”。似乎完 全推翻了我們的想法;

4、你可能還不死心,因爲在上面的測試裏,第三個表達式括號中的結果實在太詭異了,正好是“1.0”。那我們再來對比一下“(2.4- 0.2) / 0.1”和“(2.4 - 0.3) / 0.1”,前者結果是“21.999999999999996”,後者結果是“21.0”。恭喜你,做到這裏,你終於可以放棄這個無聊的測試了。

最後,我們還可以來推翻一下我們第一個測試的假設:當使用“2.3 / 0.1”時,結果爲“22.999999999999996”,出現精度丟失。也就是說,所謂“經過一次計算後,精度丟失”的假設是不成立的。

 

那麼爲什麼會出現精度丟失呢?

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

<span style="font-size:14px;">double d = 2.4;
System.out.println(d);</span>

輸出的是2.4,而不是2.3999999999999999。也就是說,不進行浮點計算的時候,在十進制裏浮點數能正確顯示。這更印證了我以上的想法,即如果浮點數參與了計算,那麼浮點數二進制與十進制間的轉換過程就會變得不可預知,並且變得不可逆。

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

 

如何使用浮點數進行精確計算

那麼能夠使用浮點數進行精確計算嗎?直接計算當然是不行啦,但是我們當然也可以通過一些方法和技巧來解決這個問題。由於浮點數計算的結果跟正確結果非常接近,你很可能想到使用四捨五入來處理結果,以得到正確的答案。這是個不錯的思路。

那麼如何實現四捨五入呢?你可能會想到Math類中的round方法,但是有個問題,round方法不能設置保留幾位小數,如果我們要保留兩位小數,我們只能像這樣實現:

<span style="font-size:14px;">public double round(double value){
  return Math.round(value*100)/100.0;
}</span>


如果這能得到正確的結果也就算了,大不了我們再想方法改進。但是非常不幸,上面的代碼並不能正常工作,如果給這個方法傳入4.015,它將返回4.01而不是4.02。

java.text.DecimalFormat也不能解決這個問題,來看下面的例子:

System.out.println(newjava.text.DecimalFormat("0.00").format(4.025));

它的輸出是4.02,而非4.03。

難道沒有解決方法了嗎?當然有的。在《Effective Java》這本書中就給出了一個解決方法。該書中也指出,float和double只能用來做科學計算或者是工程計算,在商業計算等精確計算中,我們要用java.math.BigDecimal。

BigDecimal類一個有4個方法,我們只關心對我們解決浮點型數據進行精確計算有用的方法,即

BigDecimal(double value) // 將double型數據轉換成BigDecimal型數據

思路很簡單,我們先通過BigDecimal(double value)方法,將double型數據轉換成BigDecimal數據,然後就可以正常進行精確計算了。等計算完畢後,我們可以對結果做一些處理,比如對除不盡的結果可以進行四捨五入。最後,再把結果由BigDecimal型數據轉換回double型數據。

這個思路很正確,但是如果你仔細看看API裏關於BigDecimal的詳細說明,你就會知道,如果需要精確計算,我們不能直接用double,而非要用 String來構造BigDecimal不可!所以,我們又開始關心BigDecimal類的另一個方法,即能夠幫助我們正確完成精確計算的 BigDecimal(String value)方法。

// BigDecimal(String value)能夠將String型數據轉換成BigDecimal型數據

那麼問題來了,想像一下吧,如果我們要做一個浮點型數據的加法運算,需要先將兩個浮點數轉爲String型數據,然後用 BigDecimal(String value)構造成BigDecimal,之後要在其中一個上調用add方法,傳入另一個作爲參數,然後把運算的結果(BigDecimal)再轉換爲浮點數。如果每次做浮點型數據的計算都要如此,你能夠忍受這麼煩瑣的過程嗎?至少我不能。所以最好的辦法,就是寫一個類,在類中完成這些繁瑣的轉換過程。這 樣,在我們需要進行浮點型數據計算的時候,只要調用這個類就可以了。網上已經有高手爲我們提供了一個工具類Arith來完成這些轉換操作。它提供以下靜態方法,可以完成浮點型數據的加減乘除運算和對其結果進行四捨五入的操作:

   public static double add(double v1,double v2)

   public static double sub(double v1,double v2)

   public static double mul(double v1,double v2)

   public static double div(double v1,double v2)

   public static double div(double v1,double v2,int scale)

   public static double round(double v,int scale)

      下面會附上Arith的源代碼。

      附錄:Arith源代碼

 

      import java.math.BigDecimal;
 
      /**
      * 由於Java的簡單類型不能夠精確的對浮點數進行運算,這個工具類提供精
      * 確的浮點數運算,包括加減乘除和四捨五入。
      */
 
      public class Arith{
       //默認除法運算精度
       private static final int DEF_DIV_SCALE = 10;
       //這個類不能實例化
       private Arith(){
       }
 
        /**
        * 提供精確的加法運算。
        * @param v1 被加數
        * @param v2 加數
        * @return 兩個參數的和
        */
       public static double add(doublev1,double v2){
            BigDecimal b1 = newBigDecimal(Double.toString(v1));
            BigDecimal b2 = newBigDecimal(Double.toString(v2));
            return b1.add(b2).doubleValue();
       }
       /**
        * 提供精確的減法運算。
        * @param v1 被減數
        * @param v2 減數
        * @return 兩個參數的差
        */
       public static double sub(double v1,double v2){
             BigDecimal b1 = newBigDecimal(Double.toString(v1));
             BigDecimal b2 = newBigDecimal(Double.toString(v2));
             return b1.subtract(b2).doubleValue();
       }
   /**
    * 提供精確的乘法運算。
    * @param v1 被乘數
    * @param v2 乘數
    * @return 兩個參數的積
    */
   public static double mul(double v1,double v2){
       BigDecimal b1 = new BigDecimal(Double.toString(v1));
       BigDecimal b2 = new BigDecimal(Double.toString(v2));
       return b1.multiply(b2).doubleValue();
   }
 
   /**
    * 提供(相對)精確的除法運算,當發生除不盡的情況時,精確到
    * 小數點以後10位,以後的數字四捨五入。
    * @param v1 被除數
    * @param v2 除數
    * @return 兩個參數的商
    */
   public static double div(double v1,double v2){
       return div(v1,v2,DEF_DIV_SCALE);
   }
 
   /**
    * 提供(相對)精確的除法運算。當發生除不盡的情況時,由scale參數指
    * 定精度,以後的數字四捨五入。
    * @param v1 被除數
    * @param v2 除數
    * @param scale 表示表示需要精確到小數點以後幾位。
    * @return 兩個參數的商
    */
   public static double div(double v1,double v2,int scale){
       if(scale<0){
           throw newIllegalArgumentException(
                "The scale must be apositive integer or zero");
       }
       BigDecimal b1 = new BigDecimal(Double.toString(v1));
       BigDecimal b2 = new BigDecimal(Double.toString(v2));
       return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
   }
 
   /**
    * 提供精確的小數位四捨五入處理。
    * @param v 需要四捨五入的數字
    * @param scale 小數點後保留幾位
    * @return 四捨五入後的結果
    */
   public static double round(double v,int scale){
 
       if(scale<0){
           throw new IllegalArgumentException(
                "The scale must be apositive integer or zero");
       }
       BigDecimal b = new BigDecimal(Double.toString(v));
       BigDecimal one = new BigDecimal("1");
       return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
   }
}

 

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