一張圖帶你看透BigDecimal(下)

在上半部分(一張圖帶你看透BigDecimal(上)我們對於BigDecimal的基本屬性和構造函數有了清晰的認識,在已經知道如何構造一個BigDecimal的基礎上我們再來看看如何更好的使用這個類吧。

算術運算

作爲一個數值類型,算術運算是基本功能。相應的BigDecimal也提供了基本的算術運算如加減乘除,還有一些高級運算如指數運算pow、絕對值abs和取反negate等。我們重點分析比較常用的加減乘除和指數函數pow

加法運算

在加法運算上BigDecimal提供了兩個public的方法。

1, public BigDecimal add(BigDecimal augend)

這個方法採用的邏輯比較簡單,他遵循了我們對BigDecimal的最初認識,即只要搞定四個基本屬性,這個對象就搞定了。所以在邏輯上的實現方式如下:

result.intValue/intCompact = this.intValue/intCompact + augend. intValue/intCompact
result.scale = max(this.scale, augend.scale)
result.precision = 0

 

2, public BigDecimal add(BigDecimal augend, MathContext mc)

這個方法和上面的方法只相差一個MathContext參數,依照我們之前的經驗,這個應該是在第一個方法的基礎上加入了Rounding相關的操作。事實的確如此,唯一的差異是針對零值的情況加入了處理片段。

 

減法運算

BigDecimal對於減法同樣提供了兩個public的方法,對應於加法的兩個方法。在處理邏輯上完全複用了加法的處理邏輯,針對減數進行了negate取負操作。

    public BigDecimal subtract(BigDecimal subtrahend, MathContext mc) {
        if (mc.precision == 0)
            return subtract(subtrahend);
        // share the special rounding code in add()
        return add(subtrahend.negate(), mc);
}

乘法運算

乘法運算和加法運算的思想保持一致,採用的邏輯爲:

result.intValue/intCompact = this.intValue/intCompact  *  multiplicand. intValue/intCompact
result.scale = sum(this.scale, multiplicand.scale)
result.precision = 0

 

在實際實現過程中提供了兩類方法(之所以是兩類,是因爲存在參數不同的重載),分別爲mutiplymultiplyAndRound

除法運算

除法運算採用的邏輯爲:

result.intValue/intCompact = this.intValue/intCompact  除以  divisor. intValue/intCompact

result.scale = this.scale - divisor.scale

BigDecimal的除法運算提供了5個的public方法,但是具體實現只有兩個,接下來我們具體看一下這兩個實現。

1, public BigDecimal divide(BigDecimal divisor)

這個方法在實際使用中並不多,因爲這個方法要求滿足整除的條件,如果不能整除則會拋出異常。

MathContext mc = new MathContext( (int)Math.min(this.precision() +
                                        (long)Math.ceil(10.0*divisor.precision()/3.0),
                                        Integer.MAX_VALUE),
                         RoundingMode.UNNECESSARY);
BigDecimal quotient;
try {
  quotient = this.divide(divisor, mc);
} catch (ArithmeticException e) {
  throw new ArithmeticException("Non-terminating decimal expansion; " +
                         "no exact representable decimal result.");
}

從上面的代碼可以看出來,對於沒有指定MathContext的情況會定義一個用於計算的MathContext,其中的precision爲:

Math.min(this.precision() + (long)Math.ceil(10.0*divisor.precision()/3.0),Integer.MAX_VALUE)

並且RoundingModeUNNECESSARY

這樣在無法整除的情況下,precision必然會超過定義的precision,同時由於RoundingMode的定義無法得知Rounding規則,此時拋出異常是合理的。

2, public BigDecimal divide(BigDecimal divisor, MathContext mc)

在具體實現的過程中會根據除數和被除數的類型,分別調用底層關於longBigInteger的實現,最終的實現是通過方法divideAndRound來實現的。我們主要看兩個實現

²  private static BigDecimal divideAndRound(long ldividend, long ldivisor, int scale, int roundingMode, int preferredScale)

當除數和被除數都是long類型的情況,首先找出quotientremainder

long q = ldividend / ldivisor;
long r = ldividend % ldivisor;

根據除數和被除數的符號來獲取結果的符號

qsign = ((ldividend < 0) == (ldivisor < 0)) ? 1 : -1;

如果remainder不爲0則需要處理rounding進位問題

if (r != 0) {
    boolean increment = needIncrement(ldivisor, roundingMode, qsign, q, r);
    return valueOf((increment ? q + qsign : q), scale);
}

如果remainder0則直接對於scale進行處理即可。

 

²  private static BigDecimal divideAndRound(BigInteger bdividend, BigInteger bdivisor, int scale, int roundingMode, int preferredScale)

當除數和被除數都是BigInteger的情況,我們的處理流程和long相似,不同點在於BigIntegerlong的差異。

對於找出quotientremainderlong類型可以直接使用算術運算符,而BigInteger需要使用MutableBigIntegerdivide方法

MutableBigInteger mdividend = new MutableBigInteger(bdividend.mag);
MutableBigInteger mq = new MutableBigInteger();
MutableBigInteger mdivisor = new MutableBigInteger(bdivisor.mag);
MutableBigInteger mr = mdividend.divide(mdivisor, mq);

獲取符號位的時候通過方法位而不是直接和0比較

qsign = (bdividend.signum != bdivisor.signum) ? -1 : 1;

其他操作也是相當於做一次longBigInteger的遷移,不做贅述。

 

指數運算

指數運算同樣提供兩個public的方法實現。

1, public BigDecimal pow(int n)

方法邏輯比較簡單,通過計算unscaled valuescale來構造結果的BigDecimal

其中unscaled value通過BigIntegerpow方法直接計算,scale則利用this.scale *n來表示。

if (n < 0 || n > 999999999)
    throw new ArithmeticException("Invalid operation");
// No need to calculate pow(n) if result will over/underflow.
// Don't attempt to support "supernormal" numbers.
int newScale = checkScale((long)scale * n);
return new BigDecimal(this.inflated().pow(n), newScale);

在使用的時候注意n的取值範圍即可。

2, public BigDecimal pow(int n, MathContext mc)

按照一般規律,這個方法的邏輯應該是在上一個的基礎上對結果的BigDecimal進行rounding即可。然而事實上並不是,在實際實現中引入了X3.274-1996算法,計算邏輯如下:

int mag = Math.abs(n);
// ready to carry out power calculation...
BigDecimal acc = ONE;           // accumulator
boolean seenbit = false;        // set once we've seen a 1-bit
for (int i=1;;i++) {            // for each bit [top bit ignored]
    mag += mag;                 // shift left 1 bit
    if (mag < 0) {              // top bit is set
seenbit = true;         // OK, we're off
acc = acc.multiply(lhs, workmc); // acc=acc*x
    }
    if (i == 31)
break;                  // that was the last bit
    if (seenbit)
acc=acc.multiply(acc, workmc);   // acc=acc*acc [square]
// else (!seenbit) no point in squaring ONE
}
// if negative n, calculate the reciprocal using working precision
if (n < 0) // [hence mc.precision>0]
    acc=ONE.divide(acc, workmc);

計算過程我們可以分解如下:

²  mag += mag相當於對n做左移操作

²  if(mag <0) 表示左移之後的首位爲1,這個時候首先乘以當前BigDecimal然後通過標誌位seenbit做平方操作

²  針對最後一位的1 = 2^0*1,所以只要乘一次不需要後面的平方操作所以在i=31的情況下跳出循環

²  最後判斷n<0的情況用1除以當前累積值取倒數

我們以125次方來說明以上過程

1, 對於5做左移操作,得到第一個標識位的時候爲101000…,此時i=29

2, mag <0 => seenbit = true, acc = 1*12 = 12(12^1)

3, seenbit = true => acc = 12 * 12 = 144(12^2)

4, 左移 i= 30, mag 的值爲01000…

5, mag>0 => seenbit值不變還是true

6, seenbit =true => acc = acc * acc = 144* 144(12^4)

7, 左移i=31mag的值爲1000…

8, mag<0=>seenbit = true,acc = acc * 12 = 144*144*12(12^5)

9, i = 31 => 跳出循環

 

關鍵方法

關鍵方法是指在構造方法和算術運算中會涉及到的方法和使用中用的比較多的方法,如果這個方法的邏輯構思比較值得解析,我們會在下面羅列出來進行深入瞭解。

doRound方法也屬於關鍵方法,只不過在構造函數部分已經對於實現邏輯進行了說明,這裏不再列出來。

setScale

setScale方法用於重新設置BigDecimal對象的標度,根據我們之前的理解BigDecimal四大屬性(intVal, intCompact,precision,scale)都會相應的受到影響,如scale變化則unscaled value會相應的通過乘或者除進行調整。

需要注意的是BigDecimal對象是不可變的,所以這個方法不會直接去修改當前對象而是返回一個新的對象。

我們以public BigDecimal setScale(int newScale, int roundingMode)作爲分析對象。

在實現邏輯上按照unscaled value的範圍分成兩個處理分支:

1, 通過intCompact存儲unscaled value

根據前後scale判斷是做乘或者除,如果是乘則需要考慮超過Long.MAX_VALUE的情況,如果是除則直接調用divideAndRound方法。

2, 通過intVal存儲unscaled value

邏輯和intCompact存儲類似,差別在於調用的乘和除的方法都是適用於BigInteger而上面是適用於long

compareTo

compareTo方法用於兩個BigDecimal的比較是比較頻繁使用的方法。我們使用源代碼和程序流的方式來分析邏輯。

if (scale == val.scale) {
    long xs = intCompact;
    long ys = val.intCompact;
    if (xs != INFLATED && ys != INFLATED)
         return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
}

這裏提供了一個快速返回路徑,針對兩個比較的對象標度一致的情況。由於BigDecimal可以表示成unscaled valuescale的形式,所以在scale相等的情況下我們只需要比較unscaled value即可。在這個快速返回路徑中僅僅比較了intCompact存在的情況,long類型直接使用算術

算術比較符比較即可。

再看比較的主邏輯,

int xsign = this.signum();
int ysign = val.signum();
if (xsign != ysign)
return (xsign > ysign) ? 1 : -1;

首先獲取符號位,如果符號位不等則根據符號位的大小就可以得到結果。

if (xsign == 0)
return 0;

如果符號位都等於0,則表明兩個對象都爲0,返回相等的結果。

int cmp = compareMagnitude(val);

 

我們再看看compareMagnitude裏面的實現,

long xae = (long)this.precision() - this.scale;   // [-1]
long yae = (long)val.precision() - val.scale;     // [-1]
if (xae < yae)
    return -1;
if (xae > yae)
return 1;

這又是一個快速返回路徑,通過precision – scale可以獲得整數位的長度,根據長度可以快速比較出大小。

接下來根據算出來的sdiffscale較小的進行乘十運算使得比較的雙方在scale上沒有差異,這時候再調用BigIntegercompareMagnitude方法比較

for (int i = 0; i < len1; i++) {
    int a = m1[i];
    int b = m2[i];
    if (a != b)
         return ((a & LONG_MASK) < (b & LONG_MASK)) ? -1 : 1;
}

這裏是按順序比較每個int的大小,由於比較是基於無符號的所以需要與LONG_MASK進行按位與操作將首位置爲0.

return (xsign > 0) ? cmp : -cmp;

最終我們根據符號位和上面的比較結果確定輸出結果是否需要處理。

equals

equals方法在我們很多數據結構中都會隱式的調用,如ArrayListcontains方法。而在BigDecimalequals中就隱藏着一個大坑,直接上代碼:

if (scale != xDec.scale)
return false;

看這個快速返回塊,如果這兩個BigDecimalscale不相等則判定BigDecimal不相等,這樣導致的結果是BigDecimal(“0”)BigDecimal(“0.0”) 這兩個數居然不相等,測試如下:

BigDecimal a = new BigDecimal("0.0");
BigDecimal b = BigDecimal.ZERO;
System.out.print(a.equals(b));//false

這裏也印證了前面提過的compareToequals不等價的說法,坑反正已經在了,咱別踩就行。

 

總結

我們通過BigDecimal的註釋整理出BigDecimal類的脈絡,然後按照基本屬性、創建函數、算術運算和關鍵方法的順序進行各個擊破。

通過基本屬性的解析我們瞭解了BigDecimal類的骨架就是unscaled valuescale,由此可以推斷出BigDecimal的上下限是由BigIntegerint的上下限決定。

在創建函數部分,除了我們的老朋友基於字符型的構造函數,我們還認識了基於數值型的構造函數並且知道了爲什麼double型的構造函數會出現小數精度問題,這個問題在工廠函數中使用了DoubletoString方法進行解決。

算術運算的核心是計算核心屬性,通過BigIntegerLong的相應方法計算出unscaled value,通過算術運算對於scale的約定計算出scale,最後結合MathContext的設置進行rounding操作。在除法運算中需要注意整除的問題,對於不確定能不能整除的情況一定要指定MathContext,否則可能拋出異常。在指數運算中引入了X3.274-1996算法,使用位移的方式來進行指數計算。

我們通過整個類的解讀識別出了doRoundsetScalecompareToequals這四個關鍵方法,其中doRoundsetScale是顯式調用比較多的,而compareToequals是比較容易出問題的方法。特別需要注意的是equals的相等和compareTo返回的0並不等價。

 

通過以上的解讀,相信大部分的人都可以毫無心理壓力的使用BigDecimal了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章