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啦。(它就是通過舍入方式得到正確的答案)