Java中數據處理精度丟失的問題

關於Java中數據處理精度丟失的問題

數值之間的轉換

​ Java中經常需要將一種數值類型轉換爲另外一種數值類型,下圖就給出了數值類型之間的合法轉換。

(圖源)

​ 實線代表合法轉換即無信息丟失的轉換,虛線表示轉換可能存在精度丟失問題。
在進行兩個數值的運算時,

如果兩個操作數中有一個是double類型的,另外一個自動轉換爲double類型。
如果其中一個操作數是float類型的,另外一個操作數也將自動轉換爲float類型。
如果其中一個操作數是long類型的,另外一個操作數也將自動轉換爲long類型。
否則,兩個操作數都會被轉換爲int類型。

​ 所以在對兩個不同類型操作數進行運算時,就可能存在精度丟失的問題。
​ 例如:當一個int型數值和一個float型數值進行運算時,int型操作數將會自動轉換爲float型操作數。那麼int型數值是如何轉換爲float型數值的?首先要了解他們是如何存儲在計算機中的。整型在計算機中以第一位表示符號,剩餘尾數表示數值的一串二級制數字來表示,而浮點型無論單精度還是雙精度都有相同的存儲方式:

符號位
指數位
尾數位

​ 其中符號位表示浮點數的正負,指數位表示其的階位,以移位的形勢表現,尾數爲表示其數值。其中float 1位符號位,8位指數位,23位尾數位。double 1位符號位,11位指數位,52位尾數位。
​ 所以當一個int 類型的數值轉換爲float類型是,int型數值的32位拆分爲 1位符號位,8位指數位,23位尾數位,改變了原本的表示方式,故產生了精度丟失。
​ 其他的轉換類似,當位數不夠時通過補0來湊夠位數。

java中double和float精度丟失問題

​ 這是java和其它計算機語言都會出現的問題,下面我們分析一下爲什麼會出現這個問題:
​ float和double類型主要是爲了科學計算和工程計算而設計的。他們執行二進制浮點運算,這是爲了在廣泛的數字範圍上提供較爲精確的快速近似計算而精心設計的。然而,它們並沒有提供完全精確的結果,所以我們不應該用於精確計算的場合。float和double類型尤其不適合用於貨幣運算,因爲要讓一個float或double精確的表示0.1或者10的任何其他負數次方值是不可能的(其實道理很簡單,十進制系統中能不能準確表示出1/3呢?同樣二進制系統也無法準確表示1/10)。

​ 浮點運算很少是精確的,只要是超過精度能表示的範圍就會產生誤差。往往產生誤差不是因爲數的大小,而是因爲數的精度。因此,產生的結果接近但不等於想要的結果。尤其在使用 float 和 double 作精確運算的時候要特別小心。

​ 現在我們就詳細剖析一下浮點型運算爲什麼會造成精度丟失?

首先我們要搞清楚下面兩個問題:
 
     (1) 十進制整數如何轉化爲二進制數
 
           算法很簡單。舉個例子,11表示成二進制數:
 
                    	 11/2=51
 
                       5/2=21
 
                       2/2=10
 
                       1/2=01
 
                           0結束         二進制表示爲(從下往上):1011
 
          這裏提一點:只要遇到除以後的結果爲0了就結束了,大家想一想,所有的整數除以2是不是一定能夠最終得到						0。換句話說,所有的整數轉變爲二進制數的算法會不會無限循環下去呢?絕對不會,整數永遠可以用二進制精						確表示 ,但小數就不一定了。
 
      (2) 十進制小數如何轉化爲二進制數
 
           算法是乘以2直到沒有了小數爲止。舉個例子,0.9表示成二進制數
 
                     0.9*2=1.8   取整數部分 1
 
                     0.8(1.8的小數部分)*2=1.6    取整數部分 1
 
                     0.6*2=1.2   取整數部分 1
 
                     0.2*2=0.4   取整數部分 0
 
                     0.4*2=0.8   取整數部分 0
 
                     0.8*2=1.6 取整數部分 1
 
                     0.6*2=1.2   取整數部分 0
 
                              .........      0.9二進制表示爲(從上往下): 1100100100100......
 
           注意:上面的計算過程循環了,也就是說*2永遠不可能消滅小數部分,這樣算法將無限下去。很顯然,小數的二進制表示有時是不可能精確的 。其實道理很簡單,十進制系統中能不能準確表示出1/3呢?同樣二進制系統也無法準確表示1/10。這也就解釋了爲什麼浮點型減法出現了"減不盡"的精度丟失問題。

解決方法

​ 使用《Effective Java》這本書中的BigDecmal,而且需要在構造參數使用String類型。

​ BigDecimal類有4個構造方法,但這裏只關心對解決浮點型數據進行精確計算有用的方法,即

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

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

​ 但是根據BigDecimal的詳細說明,如果需要精確計算,不能直接用double,要用 String來構造BigDecimal。所以使用BigDecimal類的另一個方法,即能夠幫助我們正確完成精確計算的 BigDecimal(String value)方法。

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

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

package com.util;
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();
    }
}

​ 附上Arith的源代碼,只要把它編譯保存好,要進行浮點數計算的時候,在你的源程序中導入Arith類就可以使用以上靜態方法來進行浮點數的精確計算了。

參考資料:

java中double和float精度丟失問題

https://blog.csdn.net/a_zhangq/article/details/90927472

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