Java中浮點型數據Float和Double進行精確計算的問題

1、浮點計算中發生精度丟失 

      大概不少有編程經驗的朋友都對這個問題不陌生了:不管你使用的是什麼編程語言,在使用浮點型數據進行精確計算時,你都有可能遇到計算結果出錯的狀況。來看下面的例子。 java

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

double a = (1.2 - 0.4) / 0.1;
System.out.println(a); 編程語言

    若是你認爲這個程序的輸出結果是「8」的話,那你就錯了。實際上,程序的輸出結果是「7.999999999999999」。好,問題來了。究竟是哪裏出了錯? ide

    浮點型數據進行精確計算時,該類問題並很多見。咱們姑且稱其爲「精度丟失」吧。你們能夠試着改一下上面的程序,你會發現一些有趣的現象: 工具

    一、若是你直接使用一個數字代替括號裏的表達式,如「0.8 / 0.1」或者「1.1 /0.1」,那麼彷佛,注意只是彷佛不會出現問題; 測試

    二、咱們可能會作第二個測試,就是對比「0.8 / 1.1」和「(1.2 - 0.4) / 1.1」的結果,沒錯,我就是這樣作的。那麼你會發現,前者的結果是「0.7272727272727273」(四捨五入後的結果),然後者的結果是 「0.7272727272727272」(沒有進行四捨五入)。能夠推測,通過一次計算後,精度丟失了; .net

    三、很好,我以爲咱們已經很接近真相了,可是接下來的第三個測試或許會讓你泄氣,我是這樣作的:對比「(2.4 - 0.1) / 0.1」、「(1.2 - 0.1) / 0.1」以及「(1.1 - 0.1) / 0.1」的結果,第一個是「22.999999999999996」,第二個是「10.999999999999998」,第三個是「10.0」。彷佛完 全推翻了咱們的想法; 翻譯

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

    最後,咱們還能夠來推翻一下咱們第一個測試的假設:當使用「2.3 / 0.1」時,結果爲「22.999999999999996」,出現精度丟失。也就是說,所謂「通過一次計算後,精度丟失」的假設是不成立的。 ip

2、爲什麼會出現精度丟失

    那麼爲何會出現精度丟失呢?在查閱了一些資料之後,我稍微有了一些頭緒,下面是本人的愚見,僅供參考。

    首先得從計算機自己去討論這個問題。咱們知道,計算機並不能識別除了二進制數據之外的任何數據。不管咱們使用何種編程語言,在何種編譯環境下工做,都要先 把源程序翻譯成二進制的機器碼後才能被計算機識別。以上面提到的狀況爲例,咱們源程序裏的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,它 的小數點移動到了一個新的位置(即浮動了)。可見,浮點數原本就是用於科學計算的,用來進行精確計算實在太不合適了。

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

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

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

public double round(double value){
    return Math.round(value*100)/100.0;
}

    若是這能獲得正確的結果也就算了,大不了咱們再想方法改進。可是很是不幸,上面的代碼並不能正常工做,若是給這個方法傳入4.015,它將返回4.01而不是4.02。

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

System.out.println(new java.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類就可使用以上靜態方法來進行浮點數的精確計算了。

附錄: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(double v1,double v2){
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2).doubleValue();
    }
    /**
     * 提供精確的減法運算。
     * @param v1 被減數
     * @param v2 減數
     * @return 兩個參數的差
     */
    public static double sub(double v1,double v2){
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(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 new IllegalArgumentException(
                "The scale must be a positive 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 a positive 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();     } };

相關文章
相關標籤/搜索