写点什么

BigDecimal 是如何搞定精度缺失的

用户头像
hasWhere
关注
发布于: 2020 年 10 月 05 日
BigDecimal是如何搞定精度缺失的

经常做金额计算的小伙伴,肯定知道计算资金要使用 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>'&#92;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
用户头像

hasWhere

关注

间歇性努力的学习渣 2018.04.20 加入

通过博客来提高下对自己的要求

评论

发布
暂无评论
BigDecimal是如何搞定精度缺失的