BigDecimal 是如何搞定精度缺失的
发布于: 2020 年 10 月 05 日

经常做金额计算的小伙伴,肯定知道计算资金要使用 BigDecimal,而不要使用 double 和 float,因为 double 和 float 会导致小数点后面计算出现精度问题。
为什么会出现精度缺失
Double dou1 = 0.1d; Double dou2 = 0.2d;//控制台输出结果(0.30000000000000004)System.out.println(dou1+dou2);复制代码
表面上我们做的是十进制的加法,实际上计算机在底层把它换算成了二进制,再做运算。但是 0.1 和 0.2 用二进制表示的话位数是无法穷尽的。因此我们看到的 0.1 用二进制表示的某数只是真实的 0.1 的一个近似数。0.2 也是这个道理。所以实际上 0.1+0.2 是两个近似数的相加,因此这个结果也就是 0.3 的近似数啦。这里不做十进制小数转二进制的详细算法,感兴趣的小伙伴可以继续深入研究。
猜测解决思路
既然是因为小数不能精确表示导致的精度缺失,那是不是把小数转化为整数进行计算后,再除以 10 的小数位次方即可解决。
//0.1的10倍转化为1Double dou1 = 1d;//0.2的10倍转化为2Double dou2 = 2d;//相乘后的结果除以10,控制台输出(0.3),没有出现精度缺失System.out.println((dou1+dou2)/100);复制代码
看看 BigDecimal 如何处理的
构造器(jdk1.8)
public BigDecimal(char[] in, int offset, int len, MathContext mc) { // protect against huge length. if (offset + len > in.length || offset < 0) throw new NumberFormatException("Bad offset or len arguments for char[] input."); // This is the primary string to BigDecimal constructor; all // incoming strings end up here; it uses explicit (inline) // parsing for speed and generates at most one intermediate // (temporary) object (a char[] array) for non-compact case.
// 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 // use array bounds checking to handle too-long, len == 0, // bad offset, etc. try { // 支持数字带有符号位,-代表负值 +代表正值 // handle the sign boolean isneg = false; // assume positive if (in[offset] == '-') { isneg = true; // leading minus means negative offset++; len--; } else if (in[offset] == '+') { // leading + allowed offset++; len--; }
// 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 //判断数字长度是否超过18位,合法的数字,数字+一个小数点+科学计数法 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; 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) //如果出现在小数点后,则规模+1 ++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) //如果出现在小数点后,则规模+1 ++scl; } else if (c == '.') { // have dot // have dot if (dot) // two dots throw new NumberFormatException(); 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) //如果出现在小数点后,则规模+1 ++scl; } else if ((c == 'e') || (c == 'E')) { exp = parseExp(in, offset, len); // Next test is required for backwards compatibility if ((int) exp != exp) // overflow throw new NumberFormatException(); break; // [saves a test] } else { throw new NumberFormatException(); } } if (prec == 0) // no digits found throw new NumberFormatException(); // Adjust scale if exp is not zero. if (exp != 0) { // had significant exponent 是否是指数 scl = adjustScale(scl, exp); } rs = isneg ? -rs : rs; int mcp = mc.precision; int drop = prec - mcp; // prec has range [1, MAX_INT], mcp has range [0, MAX_INT]; // therefore, this subtract cannot overflow if (mcp > 0 && drop > 0) { // do rounding while (drop > 0) { scl = checkScaleNonZero((long) scl - drop); rs = divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode); prec = longDigitLength(rs); drop = prec - mcp; } } } else { char coeff[] = new char[len]; for (; len > 0; offset++, len--) { c = in[offset]; // have digit if ((c >= '0' && c <= '9') || Character.isDigit(c)) { // First compact case, we need not to preserve the character // and we can just compute the value in place. if (c == '0' || Character.digit(c, 10) == 0) { if (prec == 0) { coeff[idx] = c; prec = 1; } else if (idx != 0) { coeff[idx++] = c; ++prec; } // else c must be a redundant leading zero } else { if (prec != 1 || idx != 0) ++prec; // prec unchanged if preceded by 0s coeff[idx++] = c; } if (dot) ++scl; continue; } // have dot if (c == '.') { // have dot if (dot) // two dots throw new NumberFormatException(); dot = true; continue; } // exponent expected if ((c != 'e') && (c != 'E')) throw new NumberFormatException(); exp = parseExp(in, offset, len); // Next test is required for backwards compatibility if ((int) exp != exp) // overflow throw new NumberFormatException(); break; // [saves a test] } // here when no characters left if (prec == 0) // no digits found throw new NumberFormatException(); // Adjust scale if exp is not zero. if (exp != 0) { // had significant exponent scl = adjustScale(scl, exp); } // Remove leading zeros from precision (digits count) rb = new BigInteger(coeff, isneg ? -1 : 1, prec); rs = compactValFor(rb); int mcp = mc.precision; if (mcp > 0 && (prec > mcp)) { if (rs == INFLATED) { int drop = prec - mcp; while (drop > 0) { scl = checkScaleNonZero((long) scl - drop); rb = divideAndRoundByTenPow(rb, drop, mc.roundingMode.oldMode); rs = compactValFor(rb); if (rs != INFLATED) { prec = longDigitLength(rs); break; } prec = bigDigitLength(rb); drop = prec - mcp; } } if (rs != INFLATED) { int drop = prec - mcp; while (drop > 0) { scl = checkScaleNonZero((long) scl - drop); rs = divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode); prec = longDigitLength(rs); drop = prec - mcp; } rb = null; } } } } catch (ArrayIndexOutOfBoundsException e) { throw new NumberFormatException(); } catch (NegativeArraySizeException e) { throw new NumberFormatException(); } this.scale = scl; this.precision = prec; this.intCompact = rs; this.intVal = rb; }复制代码
计算加法
//判断小数位是否一致//如果加数小数位小于被加数小数位,则加数*10^差值,然后相加后,取被加数的小数位//如果加数小数位大于被加数小数位,则被加数*10^差值,然后相加后,取加数的小数位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); } } }复制代码
/** * Returns a string representation of this {@code BigDecimal} * without an exponent field. For values with a positive scale, * the number of digits to the right of the decimal point is used * to indicate scale. For values with a zero or negative scale, * the resulting string is generated as if the value were * converted to a numerically equal value with zero scale and as * if all the trailing zeros of the zero scale value were present * in the result. * * The entire string is prefixed by a minus sign character '-' * (<tt>'\u002D'</tt>) if the unscaled value is less than * zero. No sign character is prefixed if the unscaled value is * zero or positive. * * Note that if the result of this method is passed to the * {@linkplain #BigDecimal(String) string constructor}, only the * numerical value of this {@code BigDecimal} will necessarily be * recovered; the representation of the new {@code BigDecimal} * may have a different scale. In particular, if this * {@code BigDecimal} has a negative scale, the string resulting * from this method will have a scale of zero when processed by * the string constructor. * * (This method behaves analogously to the {@code toString} * method in 1.4 and earlier releases.) * * @return a string representation of this {@code BigDecimal} * without an exponent field. * @since 1.5 * @see #toString() * @see #toEngineeringString() */ public String toPlainString() { if(scale==0) { if(intCompact!=INFLATED) { return Long.toString(intCompact); } else { return intVal.toString(); } } if(this.scale<0) { // No decimal point if(signum()==0) { return "0"; } int tailingZeros = checkScaleNonZero((-(long)scale)); StringBuilder buf; if(intCompact!=INFLATED) { buf = new StringBuilder(20+tailingZeros); buf.append(intCompact); } else { String str = intVal.toString(); buf = new StringBuilder(str.length()+tailingZeros); buf.append(str); } for (int i = 0; i < tailingZeros; i++) buf.append('0'); return buf.toString(); } String str ; if(intCompact!=INFLATED) { str = Long.toString(Math.abs(intCompact)); } else { str = intVal.abs().toString(); } return getValueString(signum(), str, scale); }
/* Returns a digit.digit string */ private String getValueString(int signum, String intString, int scale) { /* Insert decimal point */ StringBuilder buf; int insertionPoint = intString.length() - scale; if (insertionPoint == 0) { /* Point goes right before intVal */ return (signum<0 ? "-0." : "0.") + intString; } else if (insertionPoint > 0) { /* Point goes inside intVal */ buf = new StringBuilder(intString); buf.insert(insertionPoint, '.'); if (signum < 0) buf.insert(0, '-'); } else { /* We must insert zeros between point and intVal */ buf = new StringBuilder(3-insertionPoint + intString.length()); buf.append(signum<0 ? "-0." : "0."); for (int i=0; i<-insertionPoint; i++) buf.append('0'); buf.append(intString); } return buf.toString(); }复制代码
总结
通过简单分析 BigDecimal 的源码,发现猜测的思路方向是对的,但是 BigDecimal 在异常处理和兼容上考虑的非常完善,支持科学计数法,符号位,哨兵机制防止越界等。
划线
评论
复制
发布于: 2020 年 10 月 05 日阅读数: 39
版权声明: 本文为 InfoQ 作者【hasWhere】的原创文章。
原文链接:【http://xie.infoq.cn/article/a01fa113557ad82dde3bb6a61】。文章转载请联系作者。
hasWhere
关注
间歇性努力的学习渣 2018.04.20 加入
通过博客来提高下对自己的要求











评论