1. 程式人生 > >java使用BigDecimal 處理商業精度及高精度詳解

java使用BigDecimal 處理商業精度及高精度詳解

前言

之前我是寫過一篇類似筆記:

但是呢,寫的太簡單,關鍵還沒有寫到要點,所以重新寫一篇。

情形

由於公司最近要求把股票相關的資料,全部交給後端來處理,不再由前端來處理。 
股票大家都知道,這裡面的計算都是商業級別的,小數點4+位那是再正常不過啦。 
比如這樣幾組數字

2539230979.0000 //流通受限股份
8680253870 //某個股東持股數
0.4081 //某某股東所佔總股數的比例
  • 1
  • 2
  • 3

需求是這樣的:股份單位是 萬股。比例是百分之多少(%); 
所以對於股份我們需要除以10000,保留2位小數 
對於比例 是要乘以100,保留2位小數。

除法

首先我們來寫除法。

/**
 * scale 小數點保留幾位
 */
public static BigDecimal divi(double v1,double v2, int scale){ BigDecimal b1 = new BigDecimal(String.valueOf(v1)); BigDecimal b2 = new BigDecimal(String.valueOf(v2)); return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

首先我們是傳入兩個double型別的引數和精度(小數點保留的位數), 
我們再先轉為String型別後,在利用BigDecimal的構造方法來生成BigDecimal物件v1、v2。 
v1.divide(v2…)就是v1除以v2,保留scale為小數,BigDecimal.ROUND_HALF_UP

就是我們學的四捨五入。

乘法

public static BigDecimal muli(double v1, double v2, int scale){
    BigDecimal b1 = new BigDecimal(String.valueOf(v1));
    BigDecimal b2 = new BigDecimal(String.valueOf(v2));
    BigDecimal multiply = b1.multiply(b2);
    return multiply.setScale(scale, BigDecimal.ROUND_HALF_UP)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這個和除法類似,首先把v1 、v2轉成BigDecimal物件,然後呼叫BigDecimal中的multiply方法, 
這個方法不像divide可以設定精度,所以得使用setScale()方法來設定精度。

設定精度(保留幾位小數)

public static BigDecimal scale(double v1, int scale){
    //String.valueOf(v1)
    BigDecimal b1 = new BigDecimal(Double.toString(v1));
    return b1.setScale(scale, BigDecimal.ROUND_HALF_UP);
}
  • 1
  • 2
  • 3
  • 4
  • 5

這裡我要講的是Double.toString(v1)方法是把v1轉成字串。String.valueOf(v1),也是一樣的。 
那兩者的區別是什麼呢?其實String.valueOf(v1)原始碼裡就是呼叫Double.toString(v1)的方法。 
上面這種設定精度方法,有個問題: 
要是double v1 = 0.0002,執行scale(v1, 2)時,得到的答案:0.00,其實有時候我們是想儲存有效數字。

設定有效精度(保留有效位數)

    /**
     * 保留有效位(eg:0.00002 -- 得到的是0.000020)
     * 
     * @author yutao
     * @return 
     * @date 2016年11月14日下午1:27:28
     */
    public static BigDecimal validScale(double v1, int scale){
        if (scale < 0) {  
             throw new IllegalArgumentException("The scale must be a positive integer or zero");  
         }
        BigDecimal b = new BigDecimal(String.valueOf(v1));  
        BigDecimal divisor = BigDecimal.ONE;  
        MathContext mc = new MathContext(scale);
        return b.divide(divisor, mc);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

這裡就用到了MathContext類,它的構造方法有:

1、MathContext(int setPrecision)
2、MathContext(int setPrecision, RoundingMode setRoundingMode)
3、MathContext(String val)
  • 1
  • 2
  • 3

引數setPrecision是指有效位數,不是指保留小數多少位。之前就在這裡坑到過。 
引數setRoundingMode是指舍入模式。這個和BigDecimal類似。 
也就是說要想設定有效位,就是通過MathContext來設定的。 
一般常用第1、第2中構造方法

列舉常量摘要 
ROUND_CEILING 
向正無限大方向舍入的舍入模式。 
ROUND_DOWN 
向零方向舍入的舍入模式。 
ROUND_FLOOR 
向負無限大方向舍入的舍入模式。 
ROUND_HALF_DOWN 
向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向下舍入。 
ROUND_HALF_EVEN 
向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向相鄰的偶數舍入。 
ROUND_HALF_UP 
向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向上舍入。 
ROUND_UNNECESSARY 
用於斷言請求的操作具有精確結果的舍入模式,因此不需要舍入。(預設模式) 
ROUND_UP 
遠離零方向舍入的舍入模式。

BigDecimal構造方法應使用String型別的

例子


假設我們先使用Double型別的構造方法。

BigDecimal d1 = new BigDecimal(9.86);  
BigDecimal d2 = new BigDecimal(0.4);  
BigDecimal d3 = d1.divide(d2);  
System.out.println(d3); 
  • 1
  • 2
  • 3
  • 4


我們這樣執行後,會報如下異常:

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    at java.math.BigDecimal.divide(BigDecimal.java:1616)
    at common.ToolsUtil.main(ToolsUtil.java:164)
  • 1
  • 2
  • 3


原因:建立BigDecimal時,0.6和0.4是浮動型別的,浮點型放入BigDecimal內,其儲存值為

9.8599999999999994315658113919198513031005859375
0.40000000000000002220446049250313080847263336181640625
  • 1
  • 2


這兩個浮點數相除時,由於除不盡,而又沒有設定精度和保留小數點位數,導致丟擲異常。 
但是要是我們使用String構造方法就OK

BigDecimal d1 = new BigDecimal("9.86");  
BigDecimal d2 = new BigDecimal("0.4");  
BigDecimal d3 = d1.divide(d2);  
System.out.println(d3); 
  • 1
  • 2
  • 3
  • 4


為什麼可以這樣呢?接下來,我們探索BigDecimal原理: 
BigDecimal,不可變的、任意精度的有符號十進位制數。 
BigDecimal 由任意精度的整數非標度值 和 32 位的整數標度(scale) 組成。 
如果為零或正數,則標度是小數點後的位數。 
如果為負數,則將該數的非標度值乘以 10 的負 scale 次冪。 
因此,BigDecimal 表示的數值是 (unscaledValue × 10-scale)。我們知道BigDecimal有三個主要的建構函式

1
public BigDecimal(double val)

將double表示形式轉換為BigDecimal

2

public BigDecimal(int val)

將int表示形式轉換為BigDecimal

3

public BigDecimal(String val)

將字串表示形式轉換為BigDecimal
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16


通過這三個建構函式,可以把double型別,int型別,String型別構造為BigDecimal物件, 
在BigDecimal物件內通過BigIntegerintVal儲存傳遞物件數字部分,通過int scale;記錄小數點位數, 
通過int precision;記錄有效位數(預設為0)。 

BigDecimal的加減乘除就成了BigInteger與BigInteger之間的加減乘除,浮點數的計算也轉化為整形的計算, 
可以大大提供效能,並且通過BigInteger可以儲存大數字,從而實現真正大十進位制的計算, 
在整個計算過程中,還涉及scale的判斷和precision判斷從而確定最終輸出結果。 

通過上面的例子可以看出String的建構函式就是通過BigInteger記錄BigDecimal的值, 
使其計算變成BigInteger之間的計算。所以我們一般最好使用String型別的構造方法。


那如果非要使用Double型別的構造方法呢? 
我們可以利用divide設定精度的方式來做

BigDecimal d1 = new BigDecimal(9.86);  
BigDecimal d2 = new BigDecimal(0.4);  
BigDecimal d3 = d1.divide(d2 ,1 , BigDecimal.ROUND_HALF_UP);  
System.out.println(d3); 
  • 1
  • 2
  • 3
  • 4


通過/1,然後設定保留小數點方式,以及設定數字保留模式,從而得到兩個數乘積的小數部分。 
也就是給它設定好精度和舍入模式,就OK啦。(它就是通過舍入方式得到正確的答案)