BigDecimal源码分析及使用

浮点数使用计算机存储时,存在精度丢失的问题。如果遇到浮点数算术运算或比较运算时,一种推荐的做法是使用BigDecimal。
在使用BigDecimal进行浮点数运算时,根据阿里巴巴《Java开发手册》,有以下编程归约:
编程归约一:
BigDecimal等值比较.
编程归约二:
BigDecimal构造注意事项

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使用避坑

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
BigDecimalJava 中用于高精度数值计算的类,它的源码相对较为复杂。下面是对 BigDecimal源码进行简要解析: 1. 数据结构:BigDecimal 内部使用一个 int 类型的数组来存储数值的每个位。数组中的每个元素对应一段十进制数的位数,默认每个元素存储 9 位或 18 位。同时,还使用一个 int 类型的标志位来表示数值的正负。 2. 构造方法:BigDecimal 提供了多个构造方法用于创建 BigDecimal 对象。可以通过传入整数、浮点数、字符串等不同类型的参数来创建 BigDecimal 对象。 3. 运算方法:BigDecimal 提供了一系列精确的运算方法,包括加法、减法、乘法、除法等。这些方法通过调用内部的原子操作方法来完成具体的运算,保证了计算结果的精确性。 4. 精度控制:BigDecimal 提供了 setScale() 方法用于设置小数点后保留的位数,并可以选择不同的舍入规则,如四舍五入、向上取整等。这样可以灵活地控制计算结果的精度。 5. 内部操作方法:BigDecimal 内部定义了一系列的原子操作方法,用于执行具体的运算操作,如加法、减法、乘法等。这些方法会根据操作数的精度进行位数调整和对齐,保证运算的精确性。 6. 其他功能方法:BigDecimal 还提供了一些其他功能方法,如取绝对值、取反、取整等。这些方法可以方便地对 BigDecimal 对象进行操作和处理。 总的来说,BigDecimal源码实现了高精度的数值计算,通过使用内部的数据结构和原子操作方法,以及提供精度控制和其他功能方法,保证了计算结果的准确性和精确性。这使得 BigDecimal 成为处理需要高精度计算的场景中的常用工具类。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值