在進行大數運算時,遇到了運算精度不夠的問題,稍作了一下整理
0.8-0.7 != 0.7-0.6
原因 二進位制表示十進位制數造成的精度缺失,從而導致計算出現誤差;至於精度缺失的原因就要從二進位制如何表示小數說起:
十進位制小數轉化二進位制
乘2至無小數
舉個例子
初始值(取整) | 變換後(x2) | 二進位制部分 |
---|---|---|
0.8 | 1.6 | 0.1 |
0.6 | 1.2 | 0.11 |
0.2 | 0.4 | 0.110 |
0.4 | 0.8 | 0.110 |
死迴圈 |
找到了原因,接下來該解決問題:
- 首先想到的就是放大資料,用整數來表示小數,就不會有精度缺失了,
- 實際上Java本身就提供了
BigDecimal
類來處理大數運算,我們可以將數值轉換為BigDecimal物件,呼叫它的方法進行高精度運算
BigDecimal
原始碼
- 首先看一下它儲存了哪些資料,從中可以猜測該類也是通過,縮放數值來實現精確運算的;
private final BigInteger intVal;// 未縮放的值(即真實值)
private final int scale;// 縮放比例 真實值 = 整數化的值 / (10^scale)
private transient int precision;//數值的位數
private final transient long intCompact;//數值(如果數值大於Long.MAX_VALUE,值為Long.MIN_VALUE)
複製程式碼
- 新建一個
BigDecimal
物件,構造方法可以傳入多種型別的資料,一般建議傳入string型別的值來防止資料精度的損失,尤其是public BigDecimal(double val)
原始碼中有一段註釋it is generally recommended that the {@linkplain #BigDecimal(String)<tt>String</tt> constructor} be used in preference to this one
大致意思是建議使用string型別引數代替double型別引數,原因顯然是double型別帶來的精度丟失 - 接下來研究下建構函式,
BigDecimal(String val)
最終呼叫的方法是BigDecimal(str.toCharArray(),0,val.length(),MathContext.UNLIMITED)
,以下是我對改建構函式的簡化
public void BigDecimal(char[] in, int offset, int len, MathContext mc) {
int prec = 0;//數值位數(不計數值前面的0)
int scl = 0;//放大比例 真實值 = 記錄的數值
long rs = 0;//數值
BigInteger rb = null;//傳入數值長度超過18,會使用改物件儲存資料
// 取符號位
boolean isneg = false; // assume positive
if (in[offset] == '-') {
isneg = true; // leading minus means negative
offset++;
len--;
} else if (in[offset] == '+') { // leading + allowed
offset++;
len--;
}
// 取數值
boolean dot = false;//是否到小數部分
long exp = 0;//指數
char c;
boolean isCompact = len <= 18;
int idx = 0;
if (isCompact) {// 有足夠的空間計算
for (; len > 0; offset++, len--) {
c = in[offset];
if ((c == '0')) {
if (prec == 0) prec = 1;
else if (rs != 0) {
rs *= 10;
++prec;
}
if (dot) ++scl;//記錄小數位數
} else if (c >= '1' && c <= '9') {
int digit = c - '0';
if (prec != 1 || rs != 0) ++prec;
rs = rs * 10 + digit;
if (dot) ++scl;
} else if (c == '.') {//之後是小數位
if (dot) throw new NumberFormatException();//存在兩個及以上的小數點
dot = true;
} else if (Character.isDigit(c)) {//判斷char是否為數值
//嘗試轉換字元為數字
} else if ((c == 'e') || (c == 'E')) {
exp = parseExp(in, offset, len);//指數計算
if ((int) exp != exp) // overflow
throw new NumberFormatException();
break;//指數計算完成,跳出迴圈
} else {
throw new NumberFormatException();
}
}
if (prec == 0)//無數值
throw new NumberFormatException();
if (exp != 0) scl = adjustScale(scl, exp);// scl和exp抵消
rs = isneg ? -rs : rs;
int mcp = mc.getPrecision();//獲取精度位數
int drop = prec - mcp;
if (mcp > 0 && drop > 0) {
while (drop > 0) {
// 檢查縮放位數是否超出限制
scl = checkScaleNonZero((long) scl - drop);
// 三個引數一次為:數值,10^drop(表示精確到哪一位) , 舍入模式對應的常量,返回結果為舍入後的值
rs = divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode);
// 計算舍入後數值的位數
prec = longDigitLength(rs);
//再次計算需要丟棄的位數,直至符合精度要求
drop = prec - mcp;
}
}else {
// 省略,邏輯相似,但考慮到數值超int上限,使用char[]儲存數值,
// 完成後將char[]轉為BigInteget型別
}
}
// 賦值
this.scale = scl;
this.precision = prec;
this.intCompact = rs;
this.intVal = rb;
}
複製程式碼
-
剛才的建構函式中有一個引數
MathContext
沒有解釋,字面上看是指數學上下文,應該是儲存些運算相關的環境變數,在BigDecimal
中我們用到了兩個,一個是精度precision
(即參與運算的最大位數),一個是舍入模式roundingMode
,BigDecimal
預設的上下文是MathContext.UNLIMITED
:銀行家舍入,無限精度;簡單介紹下Java中的舍入模式- ROUND_UP
向遠離零的方向舍入。捨棄非零部分,並將非零捨棄部分相鄰的一位數字加一。
- ROUND_DOWN
向接近零的方向舍入。捨棄非零部分,同時不會非零捨棄部分相鄰的一位數字加一,採取擷取行為。
- ROUND_CEILING
向正無窮的方向舍入。如果為正數,舍入結果同ROUND_UP一致;如果為負數,舍入結果同ROUND_DOWN一致。注意:此模式不會減少數值大小。
- ROUND_FLOOR
向負無窮的方向舍入。如果為正數,舍入結果同ROUND_DOWN一致;如果為負數,舍入結果同ROUND_UP一致。注意:此模式不會增加數值大小。
- ROUND_HALF_UP
向“最接近”的數字舍入,如果與兩個相鄰數字的距離相等,則為向上舍入的舍入模式。如果捨棄部分>= 0.5,則舍入行為與ROUND_UP相同;否則舍入行為與ROUND_DOWN相同。這種模式也就是我們常說的我們的“四捨五入”。
- ROUND_HALF_DOWN
向“最接近”的數字舍入,如果與兩個相鄰數字的距離相等,則為向下舍入的舍入模式。如果捨棄部分> 0.5,則舍入行為與ROUND_UP相同;否則舍入行為與ROUND_DOWN相同。這種模式也就是我們常說的我們的“五舍六入”。
- ROUND_HALF_EVEN
向“最接近”的數字舍入,如果與兩個相鄰數字的距離相等,則相鄰的偶數舍入。如果捨棄部分左邊的數字奇數,則舍入行為與 ROUND_HALF_UP 相同;如果為偶數,則舍入行為與 ROUND_HALF_DOWN 相同。注意:在重複進行一系列計算時,此舍入模式可以將累加錯誤減到最小。此舍入模式也稱為“銀行家舍入法”,主要在美國使用。四捨六入,五分兩種情況,如果前一位為奇數,則入位,否則捨去。
- ROUND_UNNECESSARY
斷言請求的操作具有精確的結果,因此不需要舍入。如果對獲得精確結果的操作指定此舍入模式,則丟擲ArithmeticException
PS:
Java
預設的Math.round()
實現的是四捨五入,而.net
,和python
3中的運算包的round()
方法預設實現的銀行家舍入,使用新語言的時候感覺需要注意下他的舍入函式的舍入模式;避免採坑. -
瞭解了
BigDecimal
的資料構造過程,如何運算就顯而易見了, 詳情自己看原始碼吧;- 加減法,將兩個數的
scale
放大至相同進行加減運算 - 乘除法,數值部分乘除運算,
scale
部分加減運算
- 加減法,將兩個數的