概述
浮点数使用计算机存储时,存在精度丢失的问题。如果遇到浮点数算术运算或比较运算时,一种推荐的做法是使用BigDecimal。
在使用BigDecimal进行浮点数运算时,根据阿里巴巴《Java开发手册》,有以下编程归约:
编程归约一:
编程归约二:
BigDecimal实现原理分析
在从源码层面分析上述归约背后的原因之前,先简单梳理下BigDecimal的实现原理。阅读BigDecimal源码可知,其支持多种运算,如:算数运算、缩放运算(scale manipulation)、舍入运算、比较运算等。这里以算数运算的加法运算为例,介绍BigDecimal的实现原理。BigDecimal加法实现源码(JDK 11)如下:
public BigDecimal add(BigDecimal augend) {
if (this.intCompact != INFLATED) {
if ((augend.intCompact != INFLATED)) {
return add(this.intCompact, this.scale, augend.intCompact, augend.scale);
} else {
return add(this.intCompact, this.scale, augend.intVal, augend.scale);
}
} else {
if ((augend.intCompact != INFLATED)) {
return add(augend.intCompact, augend.scale, this.intVal, this.scale);
} else {
return add(this.intCompact, this.scale, augend.intVal, augend.scale);
}
}
}
简单分析源码可知,BigDecimal使用intCompact/intCompact和scale辅助计算。接下来,我们深入BigDecimal构造函数,看看BigDecimal的构造原理。
public BigDecimal(String val) {
// 复用字符数组构造函数
this(val.toCharArray(), 0, val.length());
}
public BigDecimal(char[] in, int offset, int len) {
this(in,offset,len,MathContext.UNLIMITED);
}
// 这里仅展示主体代码,有兴趣可以阅读源码
public BigDecimal(char[] in, int offset, int len, MathContext mc) {
...
// Use locals for all fields values until completion
int prec = 0; // record precision value
int scl = 0; // record scale value
long rs = 0; // the compact value in long
BigInteger rb = null; // the inflated value in BigInteger
try {
...
// should now be at numeric part of the significand
boolean dot = false; // true when there is a '.'
long exp = 0; // exponent
char c; // current character
boolean isCompact = (len <= MAX_COMPACT_DIGITS);
// integer significand array & idx is the index to it. The array
// is ONLY used when we can't use a compact representation.
int idx = 0;
// 仅分析可以使用long表示膨胀(浮点乘以n个10转换成整数)后的浮点数
if (isCompact) {
// First compact case, we need not to preserve the character
// and we can just compute the value in place.
for (; len > 0; offset++, len--) {
c = in[offset];
if ((c == '0')) { // have zero
if (prec == 0)
prec = 1;
else if (rs != 0) {
// 更新膨胀值
rs *= 10;
// 更新精度
++prec;
} // else digit is a redundant leading zero
if (dot)
// 更新标度
++scl;
} else if ((c >= '1' && c <= '9')) { // have digit
int digit = c - '0';
if (prec != 1 || rs != 0)
++prec; // prec unchanged if preceded by 0s
rs = rs * 10 + digit;
if (dot)
++scl;
} else if (c == '.') { // have dot
// have dot
if (dot) // two dots
throw new NumberFormatException("Character array"
+ " contains more than one decimal point.");
dot = true;
} else if (Character.isDigit(c)) { // slow path
int digit = Character.digit(c, 10);
if (digit == 0) {
if (prec == 0)
prec = 1;
else if (rs != 0) {
rs *= 10;
++prec;
} // else digit is a redundant leading zero
} else {
if (prec != 1 || rs != 0)
++prec; // prec unchanged if preceded by 0s
rs = rs * 10 + digit;
}
if (dot)
++scl;
} else if ((c == 'e') || (c == 'E')) { // scientific notation
exp = parseExp(in, offset, len);
// Next test is required for backwards compatibility
if ((int) exp != exp) // overflow
throw new NumberFormatException("Exponent overflow.");
break; // [saves a test]
} else {
throw new NumberFormatException("Character " + c
+ " is neither a decimal digit number, decimal point, nor"
+ " "e" notation exponential mark.");
}
}
if (prec == 0) // no digits found
throw new NumberFormatException("No digits found.");
// Adjust scale if exp is not zero.
if (exp != 0) { // had significant exponent
scl = adjustScale(scl, exp);
}
// 膨胀值是个非负数
rs = isneg ? -rs : rs;
...
} else {
...
}
} catch (ArrayIndexOutOfBoundsException | NegativeArraySizeException e) {
NumberFormatException nfe = new NumberFormatException();
nfe.initCause(e);
throw nfe;
}
this.scale = scl;
this.precision = prec;
this.intCompact = rs;
this.intVal = rb;
}
分析源码可知,BigDecimal使用“膨胀值(非负数)”(intVal/intCompact)、“标度”(scale)、“精度(precision)”来实现准备计算。接下来分析add方法的源码实现,看看是如何这三个度量值(作为示例,仅分析使用long类型数据表示无标度值的场景)。
private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
long sdiff = (long) scale1 - scale2;
if (sdiff == 0) {
// 如果膨胀的标度值相等,直接相加
return add(xs, ys, scale1);
} else if (sdiff < 0) {
// 如果膨胀的标度值不相等,膨胀至相同标度,再相加
int raise = checkScale(xs,-sdiff);
long scaledX = longMultiplyPowerTen(xs, raise);
if (scaledX != INFLATED) {
return add(scaledX, ys, scale2);
} else {
// 处理整数越界的情况
BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
return ((xs^ys)>=0) ? // same sign test
new BigDecimal(bigsum, INFLATED, scale2, 0)
: valueOf(bigsum, scale2, 0);
}
} else {
int raise = checkScale(ys,sdiff);
long scaledY = longMultiplyPowerTen(ys, raise);
if (scaledY != INFLATED) {
return add(xs, scaledY, scale1);
} else {
BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
return ((xs^ys)>=0) ?
new BigDecimal(bigsum, INFLATED, scale1, 0)
: valueOf(bigsum, scale1, 0);
}
}
}
分析add方法源码发现,add方法实现的基本思路就是将浮点数膨胀成整数,并记录标度值。分析源码还发现,这里并没有使用精度值。此外,这里还处理了整数和越界的问题。可见,BigDecimal是在long、BigInteger的基础上封装对float、double进行精确计算的工具类。
了解了BigDecimal实现浮点数精确计算的原理后,接下里从源码层次分析上述归约的制定原因。由于禁止使用BigDecimal(double)构造BigDecimal对象归约涉及构造函数,所有优先讨论。
禁止使用BigDecimal(double)构造BigDecimal对象
在将 double 值转化为 BigDecimal 对象时,禁止使用构造方法 BigDecimal(double)的方式。推荐使用BigDecimal(String)的方式。这是因为BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1F); 实际的存储值为:0.10000000149。同时注意,优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。推荐写法示例代码如下:
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
接下来从源码层面进行验证。
public BigDecimal(double val) {
this(val,MathContext.UNLIMITED);
}
// 注意,这里同样仅展示主体代码
public BigDecimal(double val, MathContext mc) {
// Translate the double into sign, exponent and significand
// 将doulbe翻译成符号位、指数、有效数三部分
long valBits = Double.doubleToLongBits(val);
int sign = ((valBits >> 63) == 0 ? 1 : -1);
int exponent = (int) ((valBits >> 52) & 0x7ffL);
long significand = (exponent == 0
? (valBits & ((1L << 52) - 1)) << 1
: (valBits & ((1L << 52) - 1)) | (1L << 52));
exponent -= 1075;
// At this point, val == sign * significand * 2**exponent.
...
int scl = 0;
// Calculate intVal and scale
BigInteger rb;
long compactVal = sign * significand;
if (exponent == 0) {
rb = (compactVal == INFLATED) ? INFLATED_BIGINT : null;
} else {
if (exponent < 0) {
rb = BigInteger.valueOf(5).pow(-exponent).multiply(compactVal);
scl = -exponent;
} else { // (exponent > 0)
rb = BigInteger.TWO.pow(exponent).multiply(compactVal);
}
compactVal = compactValFor(rb);
}
int prec = 0;
int mcp = mc.precision;
// 舍入处理
if (mcp > 0) { // do rounding
int mode = mc.roundingMode.oldMode;
int drop;
if (compactVal == INFLATED) {
prec = bigDigitLength(rb);
drop = prec - mcp;
while (drop > 0) {
scl = checkScaleNonZero((long) scl - drop);
rb = divideAndRoundByTenPow(rb, drop, mode);
compactVal = compactValFor(rb);
if (compactVal != INFLATED) {
break;
}
prec = bigDigitLength(rb);
drop = prec - mcp;
}
}
if (compactVal != INFLATED) {
prec = longDigitLength(compactVal);
drop = prec - mcp;
while (drop > 0) {
scl = checkScaleNonZero((long) scl - drop);
compactVal = divideAndRound(compactVal, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode);
prec = longDigitLength(compactVal);
drop = prec - mcp;
}
rb = null;
}
}
this.intVal = rb;
this.intCompact = compactVal;
this.scale = scl;
this.precision = prec;
}
分析源码可知,在使用double初始化BigDecimal会根据精度对膨胀值进行舍入处理。也就是说,使用double初始化BigDecimal无法保证准确。
等值比较使用compareTo()方法
BigDecimal等值比较应使用compareTo()方法,而不是equals()方法。《Java开发手册》给出的解释是:BigDecimal的equals()方法会比较膨胀值和标度值(1.0 与 1.00 返回结果为 false),而compareTo()则会忽略标度值。
public boolean equals(Object x) {
if (!(x instanceof BigDecimal)) {
return false;
}
BigDecimal xDec = (BigDecimal)x;
if (x == this) {
return true;
} else if (this.scale != xDec.scale) {
// 如果标度值不同,则返回false。
// 所以,1.0和 1.00不相等(前者标度值为1,后者标度值为2)
return false;
} else {
// 进行值比较
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != -9223372036854775808L) {
if (xs == -9223372036854775808L) {
xs = compactValFor(xDec.intVal);
}
return xs == s;
} else if (xs != -9223372036854775808L) {
return xs == compactValFor(this.intVal);
} else {
return this.inflated().equals(xDec.inflated());
}
}
}
public int compareTo(BigDecimal val) {
if (this.scale == val.scale) {
long xs = this.intCompact;
long ys = val.intCompact;
if (xs != -9223372036854775808L && ys != -9223372036854775808L) {
return xs != ys ? (xs > ys ? 1 : -1) : 0;
}
}
// 如果标度值不同,会将标尺值对齐后再比较
// 所以,不存在1.0和 1.00不相等的情况。
int xsign = this.signum();
int ysign = val.signum();
if (xsign != ysign) {
return xsign > ysign ? 1 : -1;
} else if (xsign == 0) {
return 0;
} else {
// compareMagnitude会对齐标度值,这里不展开,有兴趣的可以阅读相关源码
int cmp = this.compareMagnitude(val);
return xsign > 0 ? cmp : -cmp;
}
}
分析源码可知,在使用equals()方法比较两个BigDecimal时,如果标度值不同,则返回false。这对于标度值不同,值相同的场景不适用。。而compareTo()则没有这个问题。所以使用BigDecimal进行等值比较时,应使用compareTo()方法,而不是equals()方法。
参考
JDK 11 源码
《Java开发手册》嵩山版 阿里巴巴
https://blog.csdn.net/wangxufa/article/details/121666851 浮点数精度丢失分析及解决
https://developer.aliyun.com/article/785039 BigDecimal使用避坑
最后
以上就是含蓄楼房为你收集整理的BigDecimal源码分析及使用BigDecimal实现原理分析禁止使用BigDecimal(double)构造BigDecimal对象等值比较使用compareTo()方法参考的全部内容,希望文章能够帮你解决BigDecimal源码分析及使用BigDecimal实现原理分析禁止使用BigDecimal(double)构造BigDecimal对象等值比较使用compareTo()方法参考所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复