Java:利用BigDecimal類巧妙處理Double類型精度丟失

本篇要點

  • 簡單描述浮點數十進制轉二進制精度丟失的緣由。
  • 介紹幾種建立BigDecimal方式的區別。
  • 整理了高精度計算的工具類。
  • 學習了阿里巴巴Java開發手冊關於BigDecimal比較相等的規定。

經典問題:浮點數精度丟失

精度丟失的問題是在其餘計算機語言中也都會出現,float和double類型的數據在執行二進制浮點運算的時候,並無提供徹底精確的結果。產生偏差不在於數的大小,而是由於數的精度。html

關於浮點數存儲精度丟失的問題,話題過於龐大,感興趣的同窗能夠自行搜索一下:【解惑】剖析float型的內存存儲和精度丟失問題java

這裏簡單討論一下十進制數轉二進制爲何會出現精度丟失的現象,十進制數分爲整數部分和小數部分,咱們分開來看看就知道緣由爲什麼:算法

十進制整數如何轉化爲二進制整數?

將被除數每次都除以2,只要除到商爲0就能夠中止這個過程。app

5 / 2 = 2 餘 1
2 / 2 = 1 餘 0
1 / 2 = 0 餘 1 
    
// 結果爲 101

這個算法永遠都不會無限循環,整數永遠均可以使用二進制數精確表示,但小數呢?ide

十進制小數如何轉化爲二進制數?

每次將小數部分乘2,取出整數部分,若是小數部分爲0,就能夠中止這個過程。函數

0.1 * 2 = 0.2 取整數部分0
0.2 * 2 = 0.4 取整數部分0
0.4 * 2 = 0.8 取整數部分0
0.8 * 2 = 1.6 取整數部分1
0.6 * 2 = 1.2 取整數部分1
0.2 * 2 = 0.4 取整數部分0 

//... 我想寫到這就沒必要再寫了,你應該也已經發現,上面的過程已經開始循環,小數部分永遠不能爲0

這個算法有必定機率會存在無限循環,即沒法用有限長度的二進制數表示十進制的小數,這就是精度丟失問題產生的緣由。工具

如何用BigDecimal解決double精度問題?

咱們已經明白爲何精度會存在丟失現象,那麼咱們就應該知道,當某個業務場景對double數據的精度要求很是高時,就必須採起某種手段來處理這個問題,這也是BigDecimal爲何會被普遍應用於金額支付場景中的緣由啦。學習

BigDecimal類位於java.math包下,用於對超過16位有效位的數進行精確的運算。通常來講,double類型的變量能夠處理16位有效數,但實際應用中,若是超過16位,就須要BigDecimal類來操做。this

既然這樣,那用BigDecimal就可以很好解決這個問題咯?code

public static void main(String[] args) {
		// 方法1
        BigDecimal a = new BigDecimal(0.1);
        System.out.println("a --> " + a);
		// 方法2
        BigDecimal b = new BigDecimal("0.1");
        System.out.println("b --> " + b);
		// 方法3
        BigDecimal c = BigDecimal.valueOf(0.1);
        System.out.println("c --> " + c);
    }

你能夠思考一下,控制檯輸出會是啥。

a --> 0.1000000000000000055511151231257827021181583404541015625
b --> 0.1
c --> 0.1

能夠看到,使用方法一的構造函數仍然出現了精度丟失的問題,而方法二和方法三符合咱們的預期,爲何會這樣呢?

這三個方法其實對應着三種不一樣的構造函數:

// 傳入double
	public BigDecimal(double val) {
        this(val,MathContext.UNLIMITED);
    }
	// 傳入string
    public BigDecimal(String val) {
        this(val.toCharArray(), 0, val.length());
    }

    public static BigDecimal valueOf(double val) {
        // Reminder: a zero double returns '0.0', so we cannot fastpath
        // to use the constant ZERO.  This might be important enough to
        // justify a factory approach, a cache, or a few private
        // constants, later.
        // 能夠看到實際上就是第二種
        return new BigDecimal(Double.toString(val));
    }

關於這三個構造函數,JDK已經給出瞭解釋,並用Notes標註:

爲了防止之後圖片可能會存在顯示問題,這裏再記錄一下:

new BigDecimal(double val)

該方法是不可預測的,以0.1爲例,你覺得你傳了一個double類型的0.1,最後會返回一個值爲0.1的BigDecimal嗎?不會的,緣由在於,0.1沒法用有限長度的二進制數表示,沒法精確地表示爲雙精度數,最後的結果會是0.100000xxx。

new BigDecimal(String val)

該方法是徹底可預測的,也就是說你傳入一個字符串"0.1",他就會給你返回一個值徹底爲0,1的BigDecimal,官方也表示,能用這個構造函數就用這個構造函數叭。

BigDecimal.valueOf(double val)

第二種構造方式已經足夠優秀,可你仍是想傳入一個double值,怎麼辦呢?官方其實提供給你思路而且實現了它,可使用Double.toString(double val)先將double值轉爲String,再調用第二種構造方式,你能夠直接使用靜態方法:valueOf(double val)

Double的加減乘除運算工具類

BigDecimal所建立的是對象,故咱們不能使用傳統的+、-、*、/等算術運算符直接對其對象進行數學運算,而必須調用其相對應的方法。方法中的參數也必須是BigDecimal的對象。網上有不少這樣的工具類,這邊直接貼一下,邏輯不難,主要爲了簡化項目中頻繁互相轉化的問題。

/**
 * 用於高精確處理經常使用的數學運算
 */
public class ArithmeticUtils {
    //默認除法運算精度
    private static final int DEF_DIV_SCALE = 10;

    /**
     * 提供精確的加法運算
     *
     * @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 BigDecimal add(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2);
    }

    /**
     * 提供精確的加法運算
     *
     * @param v1    被加數
     * @param v2    加數
     * @param scale 保留scale 位小數
     * @return 兩個參數的和
     */
    public static String add(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精確的減法運算
     *
     * @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 BigDecimal sub(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2);
    }

    /**
     * 提供精確的減法運算
     *
     * @param v1    被減數
     * @param v2    減數
     * @param scale 保留scale 位小數
     * @return 兩個參數的差
     */
    public static String sub(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精確的乘法運算
     *
     * @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();
    }

    /**
     * 提供精確的乘法運算
     *
     * @param v1 被乘數
     * @param v2 乘數
     * @return 兩個參數的積
     */
    public static BigDecimal mul(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2);
    }

    /**
     * 提供精確的乘法運算
     *
     * @param v1    被乘數
     * @param v2    乘數
     * @param scale 保留scale 位小數
     * @return 兩個參數的積
     */
    public static double mul(double v1, double v2, int scale) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return round(b1.multiply(b2).doubleValue(), scale);
    }

    /**
     * 提供精確的乘法運算
     *
     * @param v1    被乘數
     * @param v2    乘數
     * @param scale 保留scale 位小數
     * @return 兩個參數的積
     */
    public static String mul(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供(相對)精確的除法運算,當發生除不盡的狀況時,精確到
     * 小數點之後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();
    }

    /**
     * 提供(相對)精確的除法運算。當發生除不盡的狀況時,由scale參數指
     * 定精度,之後的數字四捨五入
     *
     * @param v1    被除數
     * @param v2    除數
     * @param scale 表示須要精確到小數點之後幾位
     * @return 兩個參數的商
     */
    public static String div(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v1);
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精確的小數位四捨五入處理
     *
     * @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));
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    /**
     * 提供精確的小數位四捨五入處理
     *
     * @param v     須要四捨五入的數字
     * @param scale 小數點後保留幾位
     * @return 四捨五入後的結果
     */
    public static String round(String v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = new BigDecimal(v);
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 取餘數
     *
     * @param v1    被除數
     * @param v2    除數
     * @param scale 小數點後保留幾位
     * @return 餘數
     */
    public static String remainder(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 取餘數  BigDecimal
     *
     * @param v1    被除數
     * @param v2    除數
     * @param scale 小數點後保留幾位
     * @return 餘數
     */
    public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 比較大小
     * 阿里巴巴開發規範明確:比較BigDecimal的等值須要使用compareTo,不可用equals
     * equals會比較值和精度,compareTo會忽略精度
     * @param v1 被比較數
     * @param v2 比較數
     * @return 若是v1 大於v2 則 返回true 不然false
     */
    public static boolean compare(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        int bj = b1.compareTo(b2);
        boolean res;
        if (bj > 0)
            res = true;
        else
            res = false;
        return res;
    }
}

阿里巴巴Java開發手冊關於BigDecimal的規定

【強制】如上所示BigDecimal的等值比較應使用compareTo()方法,而不是equals()方法。

說明:equals()方法會比較值和精度(1.0和1.00返回結果爲false),而compareTo()則會忽略精度。

關於這一點,咱們來看一個例子就明白了:

public static void main(String[] args) {
        BigDecimal a = new BigDecimal("1");
        BigDecimal b = new BigDecimal("1.0");
        System.out.println(a.equals(b)); // false
        System.out.println(a.compareTo(b)); //0 表示相等
    }

JDK中對這兩個方法的解釋是這樣的:

  • 使用compareTo方法,兩個值相等可是精度不一樣的BigDecimal對象會被認爲是相等的,好比2.0和2.00。建議使用x.compareTo(y) <op> 0來表示(<, == , > , >= , != , <=)中的其中一個關係, 就表示運算符。
  • equals方法與compareTo方法不一樣,此方法僅在兩個BigDecimal對象的值和精度都相等時才被認爲是相等的,如2.0和2.00就是不相等的。

參考閱讀

相關文章
相關標籤/搜索