溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

BigDecimal類的用法

發(fā)布時(shí)間:2020-05-26 15:36:53 來(lái)源:億速云 閱讀:555 作者:鴿子 欄目:編程語(yǔ)言

算術(shù)運(yùn)算

作為一個(gè)數(shù)值類型,算術(shù)運(yùn)算是基本功能。相應(yīng)的BigDecimal也提供了基本的算術(shù)運(yùn)算如加減乘除,還有一些高級(jí)運(yùn)算如指數(shù)運(yùn)算pow、絕對(duì)值abs和取反negate等。我們重點(diǎn)分析比較常用的加減乘除和指數(shù)函數(shù)pow。

加法運(yùn)算

在加法運(yùn)算上BigDecimal提供了兩個(gè)public的方法。

1, public BigDecimal add(BigDecimal augend)。

這個(gè)方法采用的邏輯比較簡(jiǎn)單,他遵循了我們對(duì)BigDecimal的最初認(rèn)識(shí),即只要搞定四個(gè)基本屬性,這個(gè)對(duì)象就搞定了。所以在邏輯上的實(shí)現(xiàn)方式如下:

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)

這個(gè)方法和上面的方法只相差一個(gè)MathContext參數(shù),依照我們之前的經(jīng)驗(yàn),這個(gè)應(yīng)該是在第一個(gè)方法的基礎(chǔ)上加入了Rounding相關(guān)的操作。事實(shí)的確如此,唯一的差異是針對(duì)零值的情況加入了處理片段。

 

減法運(yùn)算

BigDecimal對(duì)于減法同樣提供了兩個(gè)public的方法,對(duì)應(yīng)于加法的兩個(gè)方法。在處理邏輯上完全復(fù)用了加法的處理邏輯,針對(duì)減數(shù)進(jìn)行了negate取負(fù)操作。

    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);
}

乘法運(yùn)算

乘法運(yùn)算和加法運(yùn)算的思想保持一致,采用的邏輯為:

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

 

在實(shí)際實(shí)現(xiàn)過(guò)程中提供了兩類方法(之所以是兩類,是因?yàn)榇嬖趨?shù)不同的重載),分別為mutiplymultiplyAndRound。

除法運(yùn)算

除法運(yùn)算采用的邏輯為:

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

result.scale = this.scale - divisor.scale

BigDecimal的除法運(yùn)算提供了5個(gè)的public方法,但是具體實(shí)現(xiàn)只有兩個(gè),接下來(lái)我們具體看一下這兩個(gè)實(shí)現(xiàn)。

1, public BigDecimal divide(BigDecimal divisor)

這個(gè)方法在實(shí)際使用中并不多,因?yàn)檫@個(gè)方法要求滿足整除的條件,如果不能整除則會(huì)拋出異常。

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.");
}

從上面的代碼可以看出來(lái),對(duì)于沒(méi)有指定MathContext的情況會(huì)定義一個(gè)用于計(jì)算的MathContext,其中的precision為:

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

并且RoundingModeUNNECESSARY。

這樣在無(wú)法整除的情況下,precision必然會(huì)超過(guò)定義的precision,同時(shí)由于RoundingMode的定義無(wú)法得知Rounding規(guī)則,此時(shí)拋出異常是合理的。

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

在具體實(shí)現(xiàn)的過(guò)程中會(huì)根據(jù)除數(shù)和被除數(shù)的類型,分別調(diào)用底層關(guān)于longBigInteger的實(shí)現(xiàn),最終的實(shí)現(xiàn)是通過(guò)方法divideAndRound來(lái)實(shí)現(xiàn)的。我們主要看兩個(gè)實(shí)現(xiàn)

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

當(dāng)除數(shù)和被除數(shù)都是long類型的情況,首先找出quotientremainder

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

根據(jù)除數(shù)和被除數(shù)的符號(hào)來(lái)獲取結(jié)果的符號(hào)

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

如果remainder不為0則需要處理rounding進(jìn)位問(wèn)題

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

如果remainder0則直接對(duì)于scale進(jìn)行處理即可。

 

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

當(dāng)除數(shù)和被除數(shù)都是BigInteger的情況,我們的處理流程和long相似,不同點(diǎn)在于BigIntegerlong的差異。

對(duì)于找出quotientremainder,long類型可以直接使用算術(shù)運(yùn)算符,而BigInteger需要使用MutableBigIntegerdivide方法

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

獲取符號(hào)位的時(shí)候通過(guò)方法位而不是直接和0比較

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

其他操作也是相當(dāng)于做一次longBigInteger的遷移,不做贅述。

 

指數(shù)運(yùn)算

指數(shù)運(yùn)算同樣提供兩個(gè)public的方法實(shí)現(xiàn)。

1, public BigDecimal pow(int n)

方法邏輯比較簡(jiǎn)單,通過(guò)計(jì)算unscaled valuescale來(lái)構(gòu)造結(jié)果的BigDecimal。

其中unscaled value通過(guò)BigIntegerpow方法直接計(jì)算,scale則利用this.scale *n來(lái)表示。

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);

在使用的時(shí)候注意n的取值范圍即可。

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

按照一般規(guī)律,這個(gè)方法的邏輯應(yīng)該是在上一個(gè)的基礎(chǔ)上對(duì)結(jié)果的BigDecimal進(jìn)行rounding即可。然而事實(shí)上并不是,在實(shí)際實(shí)現(xiàn)中引入了X3.274-1996算法,計(jì)算邏輯如下:

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);

計(jì)算過(guò)程我們可以分解如下:

2  mag += mag相當(dāng)于對(duì)n做左移操作

2  if(mag <0) 表示左移之后的首位為1,這個(gè)時(shí)候首先乘以當(dāng)前BigDecimal然后通過(guò)標(biāo)志位seenbit做平方操作

2  針對(duì)最后一位的1 = 2^0*1,所以只要乘一次不需要后面的平方操作所以在i=31的情況下跳出循環(huán)

2  最后判斷n<0的情況用1除以當(dāng)前累積值取倒數(shù)

我們以125次方來(lái)說(shuō)明以上過(guò)程

1, 對(duì)于5做左移操作,得到第一個(gè)標(biāo)識(shí)位的時(shí)候?yàn)?/span>101000…,此時(shí)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=31,mag的值為1000…

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

9, i = 31 => 跳出循環(huán)

 

關(guān)鍵方法

關(guān)鍵方法是指在構(gòu)造方法和算術(shù)運(yùn)算中會(huì)涉及到的方法和使用中用的比較多的方法,如果這個(gè)方法的邏輯構(gòu)思比較值得解析,我們會(huì)在下面羅列出來(lái)進(jìn)行深入了解。

doRound方法也屬于關(guān)鍵方法,只不過(guò)在構(gòu)造函數(shù)部分已經(jīng)對(duì)于實(shí)現(xiàn)邏輯進(jìn)行了說(shuō)明,這里不再列出來(lái)。

setScale

setScale方法用于重新設(shè)置BigDecimal對(duì)象的標(biāo)度,根據(jù)我們之前的理解BigDecimal四大屬性(intVal, intCompact,precision,scale)都會(huì)相應(yīng)的受到影響,如scale變化則unscaled value會(huì)相應(yīng)的通過(guò)乘或者除進(jìn)行調(diào)整。

需要注意的是BigDecimal對(duì)象是不可變的,所以這個(gè)方法不會(huì)直接去修改當(dāng)前對(duì)象而是返回一個(gè)新的對(duì)象。

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

在實(shí)現(xiàn)邏輯上按照unscaled value的范圍分成兩個(gè)處理分支:

1, 通過(guò)intCompact存儲(chǔ)unscaled value

根據(jù)前后scale判斷是做乘或者除,如果是乘則需要考慮超過(guò)Long.MAX_VALUE的情況,如果是除則直接調(diào)用divideAndRound方法。

2, 通過(guò)intVal存儲(chǔ)unscaled value

邏輯和intCompact存儲(chǔ)類似,差別在于調(diào)用的乘和除的方法都是適用于BigInteger而上面是適用于long。

compareTo

compareTo方法用于兩個(gè)BigDecimal的比較是比較頻繁使用的方法。我們使用源代碼和程序流的方式來(lái)分析邏輯。

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

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

算術(shù)比較符比較即可。

再看比較的主邏輯,

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

首先獲取符號(hào)位,如果符號(hào)位不等則根據(jù)符號(hào)位的大小就可以得到結(jié)果。

if (xsign == 0)
return 0;

如果符號(hào)位都等于0,則表明兩個(gè)對(duì)象都為0,返回相等的結(jié)果。

int cmp = compareMagnitude(val);

 

我們?cè)倏纯?/span>compareMagnitude里面的實(shí)現(xiàn),

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;

這又是一個(gè)快速返回路徑,通過(guò)precision – scale可以獲得整數(shù)位的長(zhǎng)度,根據(jù)長(zhǎng)度可以快速比較出大小。

接下來(lái)根據(jù)算出來(lái)的sdiff對(duì)scale較小的進(jìn)行乘十運(yùn)算使得比較的雙方在scale上沒(méi)有差異,這時(shí)候再調(diào)用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;
}

這里是按順序比較每個(gè)int的大小,由于比較是基于無(wú)符號(hào)的所以需要與LONG_MASK進(jìn)行按位與操作將首位置為0.

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

最終我們根據(jù)符號(hào)位和上面的比較結(jié)果確定輸出結(jié)果是否需要處理。

equals

equals方法在我們很多數(shù)據(jù)結(jié)構(gòu)中都會(huì)隱式的調(diào)用,如ArrayListcontains方法。而在BigDecimalequals中就隱藏著一個(gè)大坑,直接上代碼:

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

看這個(gè)快速返回塊,如果這兩個(gè)BigDecimalscale不相等則判定BigDecimal不相等,這樣導(dǎo)致的結(jié)果是BigDecimal(“0”)BigDecimal(“0.0”) 這兩個(gè)數(shù)居然不相等,測(cè)試如下:

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

這里也印證了前面提過(guò)的compareToequals不等價(jià)的說(shuō)法,坑反正已經(jīng)在了,咱別踩就行。

 

總結(jié)

我們通過(guò)BigDecimal的注釋整理出BigDecimal類的脈絡(luò),然后按照基本屬性、創(chuàng)建函數(shù)、算術(shù)運(yùn)算和關(guān)鍵方法的順序進(jìn)行各個(gè)擊破。

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

在創(chuàng)建函數(shù)部分,除了我們的老朋友基于字符型的構(gòu)造函數(shù),我們還認(rèn)識(shí)了基于數(shù)值型的構(gòu)造函數(shù)并且知道了為什么double型的構(gòu)造函數(shù)會(huì)出現(xiàn)小數(shù)精度問(wèn)題,這個(gè)問(wèn)題在工廠函數(shù)中使用了DoubletoString方法進(jìn)行解決。

算術(shù)運(yùn)算的核心是計(jì)算核心屬性,通過(guò)BigIntegerLong的相應(yīng)方法計(jì)算出unscaled value,通過(guò)算術(shù)運(yùn)算對(duì)于scale的約定計(jì)算出scale,最后結(jié)合MathContext的設(shè)置進(jìn)行rounding操作。在除法運(yùn)算中需要注意整除的問(wèn)題,對(duì)于不確定能不能整除的情況一定要指定MathContext,否則可能拋出異常。在指數(shù)運(yùn)算中引入了X3.274-1996算法,使用位移的方式來(lái)進(jìn)行指數(shù)計(jì)算。

我們通過(guò)整個(gè)類的解讀識(shí)別出了doRoundsetScale,compareToequals這四個(gè)關(guān)鍵方法,其中doRoundsetScale是顯式調(diào)用比較多的,而compareToequals是比較容易出問(wèn)題的方法。特別需要注意的是equals的相等和compareTo返回的0并不等價(jià)。

 

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


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI