阿里巴巴為什麼建議使用BigDecimal進行浮點數運算

程式設計師xiaozhang發表於2023-03-26

本文先引入一個例子,星期天你和女朋友去逛街,看到一家奶茶店。女朋友想喝奶茶了,你就去買了杯奶茶,然後你問了一下價格。店員說奶茶0.9元一杯。然後你給了1元錢。這個時候你忽然問了一下女友。服務員該找我們多少錢呢?女友說你個小傻瓜當然是0.1元啊。作為一個”嚴謹“的程式猿,這時你拿起電腦寫了個簡單計算如下:

看到計算結果不是0.1這個時候你有點慌了,女朋友怎麼會錯呢?但是你沒有猶豫女朋友說的怎麼會錯呢,女朋友說的一定是對的。一定是電腦計算錯了,然後你毫不猶豫把電腦扔了。說了一句對對對該找0.1元。【PS奶茶目前當然不可能1元錢,本文為了演示計算效果所以定義奶茶1元,因為不是所有的浮點運算都會有這樣的問題】。

千萬別小看這些一點點的計算誤差,假如我說假如全國人民一人送你0.1元你能實現財富自由了【呸,別做白日夢了好好搬磚去吧】

好了聊文章的主題BigDecimal這個知識點,這個知識點應該很多人都知道了,我感覺很有有用就聊一下。阿里巴巴開發手冊中提到:

使用BigDecimal來定義值,再進行浮點數的運算操作。

 

注意:BigDecimal(double)存在精度損失的風險,在精確計算或比較的場景中可能會導致業務邏輯異常。如:BigDecimal g = new BigDecimal(0.1f)。實際儲存的值為:0.10000000149011611938 。

      float a = 1.0f -0.9f;
        float b = 0.9f-0.8f ;
        System.out.println(a);
        System.out.println(b);
        System.out.println(a==b);

為什麼浮點數 float 或 double 運算的時候會有精度丟失的風險呢?

這個和計算機儲存浮點數的機制有很大關係。我們知道計算機是二進位制的,而且計算機在表示一個數字時,寬度是有限的,無限迴圈的小數儲存在計算機時,只能被截斷,所以就會導致小數精度發生損失的情況。

那怎麼解決精度丟失的問題呢?那就是本文要說的BigDecimal。用它可以實現對浮點的計算,解決精度丟失的問題。如下用差不多相同的程式碼結果就是對的【PS一定要記住女朋友說的就是對的就是0.1】。

BigDecimal建立方法,請先看注意事項:

阿里官方:【強制】禁止使用構造方法BigDecimal(double)的方式把double值轉換為BigDecimal物件

 
正確的建立方法:優先推薦入參為String的構造方法,或者使用BigDecimal的valueOf方法,此方法內部其實執行了Double的toString。而Double的toString按double實際能表達的精度對尾數進行了攔截。如下推薦使用:

BigDecimal a = new BigDecimal("0.1"); 

BigDecimal  b = BigDecimal.valueOf(0.1);

 

開發中用到計算也就是加減乘除,BigDecimal中相關方法如下:

add 方法用於將兩個 BigDecimal 物件相加。subtract 方法用於將兩個 BigDecimal 物件相減。multiply 方法用於將兩個 BigDecimal 物件相乘,divide 方法用於將兩個 BigDecimal 物件相除。

這裡需要注意的是,在我們使用 divide 方法的時候儘量使用 3 個引數方法。其中 scale 表示要保留幾位小數,roundingMode 代表保留規則。具體可以看原始碼,原始碼中介紹的很清楚了,並且給了各種例子如下:

大小的比較

a.compareTo(b) : 返回 -1 說明 a 小於 b,0 說明a 等於 b , 1 說明 a 大於 b。用法如下:

注意:BigDecimal的比較不能使用equals進行比較【原因還是因為精度的問題】,一定要使用compareTo比較,如下:

BigDecilmal計算相關的工具類。

add方法:

public static BigDecimal add(Number v1, Number v2) {
        return add(new Number[]{v1, v2});
    }
    public static BigDecimal add(Number... values) {
        if (ArrayUtil.isEmpty(values)) {
            return BigDecimal.ZERO;
        }
        Number value = values[0];
        BigDecimal result = new BigDecimal(null == value ? "0" : value.toString());
        for (int i = 1; i < values.length; i++) {
            value = values[i];
            if (null != value) {
                result = result.add(new BigDecimal(value.toString()));
            }
        }
        return result;
    }

substract方法:

public static BigDecimal sub(Number v1, Number v2) {
        return sub(new Number[]{v1, v2});
    }
    public static BigDecimal sub(Number... values) {
        if (ArrayUtil.isEmpty(values)) {
            return BigDecimal.ZERO;
        }
        Number value = values[0];
        BigDecimal result = new BigDecimal(null == value ? "0" : value.toString());
        for (int i = 1; i < values.length; i++) {
            value = values[i];
            if (null != value) {
                result = result.subtract(new BigDecimal(value.toString()));
            }
        }
        return result;
    }

multiply 乘法:

public static BigDecimal mul(Number v1, Number v2) {
        return mul(new Number[]{v1, v2});
    }
    public static BigDecimal mul(Number... values) {
        if (ArrayUtil.isEmpty(values)) {
            return BigDecimal.ZERO;
        }
        Number value = values[0];
        BigDecimal result = new BigDecimal(null == value ? "0" : value.toString());
        for (int i = 1; i < values.length; i++) {
            value = values[i];
            result = result.multiply(new BigDecimal(null == value ? "0" : value.toString()));
        }
        return result;
    }

divide 除法:

public static BigDecimal div(BigDecimal v1, BigDecimal v2,Integer scale,RoundingMode roundingMode) {
        if (null == v1) {
            return BigDecimal.ZERO;
        }
        if (scale < 0) {
            scale = -scale;
        }
        return v1.divide(v2, scale, roundingMode);
    }

謝謝點贊,也歡迎你分享給其他的開發者,讓更多的人知道。當然也歡迎未關注的小夥伴關注。

 

 
11

相關文章