您好,登錄后才能下訂單哦!
算術運算
作為一個數值類型,算術運算是基本功能。相應的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
在實際實現過程中提供了兩類方法(之所以是兩類,是因為存在參數不同的重載),分別為mutiply和multiplyAndRound。
除法運算采用的邏輯為:
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)
并且RoundingMode為UNNECESSARY。
這樣在無法整除的情況下,precision必然會超過定義的precision,同時由于RoundingMode的定義無法得知Rounding規則,此時拋出異常是合理的。
2, public BigDecimal divide(BigDecimal divisor, MathContext mc)
在具體實現的過程中會根據除數和被除數的類型,分別調用底層關于long和BigInteger的實現,最終的實現是通過方法divideAndRound來實現的。我們主要看兩個實現
2 private static BigDecimal divideAndRound(long ldividend, long ldivisor, int scale, int roundingMode, int preferredScale)
當除數和被除數都是long類型的情況,首先找出quotient和remainder
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); }
如果remainder為0則直接對于scale進行處理即可。
2 private static BigDecimal divideAndRound(BigInteger bdividend, BigInteger bdivisor, int scale, int roundingMode, int preferredScale)
當除數和被除數都是BigInteger的情況,我們的處理流程和long相似,不同點在于BigInteger和long的差異。
對于找出quotient和remainder,long類型可以直接使用算術運算符,而BigInteger需要使用MutableBigInteger的divide方法
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;
其他操作也是相當于做一次long到BigInteger的遷移,不做贅述。
指數運算同樣提供兩個public的方法實現。
1, public BigDecimal pow(int n)
方法邏輯比較簡單,通過計算unscaled value和scale來構造結果的BigDecimal。
其中unscaled value通過BigInteger的pow方法直接計算,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);
計算過程我們可以分解如下:
2 mag += mag相當于對n做左移操作
2 if(mag <0) 表示左移之后的首位為1,這個時候首先乘以當前BigDecimal然后通過標志位seenbit做平方操作
2 針對最后一位的1 = 2^0*1,所以只要乘一次不需要后面的平方操作所以在i=31的情況下跳出循環
2 最后判斷n<0的情況用1除以當前累積值取倒數
我們以12的5次方來說明以上過程
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=31,mag的值為1000…
8, mag<0=>seenbit = true,acc = acc * 12 = 144*144*12(12^5)
9, i = 31 => 跳出循環
關鍵方法是指在構造方法和算術運算中會涉及到的方法和使用中用的比較多的方法,如果這個方法的邏輯構思比較值得解析,我們會在下面羅列出來進行深入了解。
doRound方法也屬于關鍵方法,只不過在構造函數部分已經對于實現邏輯進行了說明,這里不再列出來。
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方法用于兩個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 value和scale的形式,所以在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可以獲得整數位的長度,根據長度可以快速比較出大小。
接下來根據算出來的sdiff對scale較小的進行乘十運算使得比較的雙方在scale上沒有差異,這時候再調用BigInteger的compareMagnitude方法比較
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方法在我們很多數據結構中都會隱式的調用,如ArrayList的contains方法。而在BigDecimal的equals中就隱藏著一個大坑,直接上代碼:
if (scale != xDec.scale) return false;
看這個快速返回塊,如果這兩個BigDecimal的scale不相等則判定BigDecimal不相等,這樣導致的結果是BigDecimal(“0”)和BigDecimal(“0.0”) 這兩個數居然不相等,測試如下:
BigDecimal a = new BigDecimal("0.0"); BigDecimal b = BigDecimal.ZERO; System.out.print(a.equals(b));//false
這里也印證了前面提過的compareTo和equals不等價的說法,坑反正已經在了,咱別踩就行。
我們通過BigDecimal的注釋整理出BigDecimal類的脈絡,然后按照基本屬性、創建函數、算術運算和關鍵方法的順序進行各個擊破。
通過基本屬性的解析我們了解了BigDecimal類的骨架就是unscaled value和scale,由此可以推斷出BigDecimal的上下限是由BigInteger和int的上下限決定。
在創建函數部分,除了我們的老朋友 – 基于字符型的構造函數,我們還認識了基于數值型的構造函數并且知道了為什么double型的構造函數會出現小數精度問題,這個問題在工廠函數中使用了Double的toString方法進行解決。
算術運算的核心是計算核心屬性,通過BigInteger和Long的相應方法計算出unscaled value,通過算術運算對于scale的約定計算出scale,最后結合MathContext的設置進行rounding操作。在除法運算中需要注意整除的問題,對于不確定能不能整除的情況一定要指定MathContext,否則可能拋出異常。在指數運算中引入了X3.274-1996算法,使用位移的方式來進行指數計算。
我們通過整個類的解讀識別出了doRound,setScale,compareTo和equals這四個關鍵方法,其中doRound和setScale是顯式調用比較多的,而compareTo和equals是比較容易出問題的方法。特別需要注意的是equals的相等和compareTo返回的0并不等價。
通過以上的解讀,相信大部分的人都可以毫無心理壓力的使用BigDecimal了。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。