您好,登录后才能下订单哦!
算术运算
作为一个数值类型,算术运算是基本功能。相应的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来实现的。我们主要看两个实现
² 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进行处理即可。
² 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);
计算过程我们可以分解如下:
² mag += mag相当于对n做左移操作
² if(mag <0) 表示左移之后的首位为1,这个时候首先乘以当前BigDecimal然后通过标志位seenbit做平方操作
² 针对最后一位的1 = 2^0*1,所以只要乘一次不需要后面的平方操作所以在i=31的情况下跳出循环
² 最后判断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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。