BigDecimal 为什么可以不丢失精度?

大家好,今天咱们来聊聊 Java 中的 BigDecimal。在金融领域,数据的精确性相当重要,一个小数点的误差可能就意味着几百万甚至几千万的损失。而 BigDecimal 就是专门用来解决这种高精度计算问题的。今天,我就带大家深入了解一下,为什么 BigDecimal 能做到不丢失精度。

一、浮点数的“坑”:精度丢失

在 Java 中,我们通常用 floatdouble 来表示浮点数。但它们有一个致命的缺陷——精度丢失。比如,0.1 + 0.2 的结果并不是 0.3,而是 0.30000000000000004。这是因为在计算机内部,浮点数是用二进制表示的,而某些十进制小数无法精确地转换为二进制,从而导致了精度问题。

这种问题在金融领域是绝对不能容忍的。想象一下,银行账户余额显示为 999.999999999999,而不是 1000,用户会怎么想?所以,我们需要一种能够精确表示和计算小数的数据类型,这就是 BigDecimal 的用武之地。

二、BigDecimal 的“秘密武器”

BigDecimal 是 Java 中用来表示高精度小数的类,它内部使用了 BigInteger 来存储数值,并通过一个 scale 属性来记录小数点的位置。简单来说,BigDecimal 把一个小数拆成了两部分:整数部分和小数点的位置。

举个例子,2.36BigDecimal 中会被表示为:

  • 整数部分:236(用 BigInteger 存储)
  • 小数点位置:2(表示小数点后有两位)

这样一来,BigDecimal 就可以精确地表示任何小数,而不用担心精度丢失的问题。

三、BigDecimal 的加法运算

我们来看一个简单的例子,理解一下 BigDecimal 是如何进行加法运算的:

BigDecimal bigDecimal1 = BigDecimal.valueOf(2.36);
BigDecimal bigDecimal2 = BigDecimal.valueOf(3.5);
BigDecimal result = bigDecimal1.add(bigDecimal2);
System.out.println(result); // 输出:5.86

在这个例子中,bigDecimal1bigDecimal2 的小数位数不同(一个是两位小数,一个是两位小数)。BigDecimal 在进行加法运算时,会先将两个数的小数位数对齐,然后进行整数加法运算。

具体步骤如下:

  1. 对齐小数位数:将 3.5 转换为 3.50,这样两个数的小数位数就一致了。
  2. 整数加法:将 236350 相加,得到 586
  3. 设置小数点位置:根据小数位数(这里是两位),将结果表示为 5.86

这个过程的核心在于,BigDecimal 把小数运算转换为了整数运算,而整数运算是不会丢失精度的。

四、BigDecimal 的内部实现

BigDecimal 的内部实现非常精巧。它使用了 BigInteger 来存储整数部分,这样可以保证数值的范围几乎不受限制。同时,它通过 scale 属性来记录小数点的位置,从而实现了高精度的小数运算。

我们再来看一个稍微复杂一点的例子,理解一下 BigDecimal 是如何处理不同小数位数的加法运算的:

BigDecimal bigDecimal1 = new BigDecimal("2.36");
BigDecimal bigDecimal2 = new BigDecimal("3.5");
BigDecimal result = bigDecimal1.add(bigDecimal2);
System.out.println(result); // 输出:5.86

在这个例子中,bigDecimal1 的小数位数是两位,而 bigDecimal2 的小数位数是一位。BigDecimal 在进行加法运算时,会先将两个数的小数位数对齐,然后进行整数加法运算。

具体步骤如下:

  1. 对齐小数位数:将 3.5 转换为 3.50,这样两个数的小数位数就一致了。
  2. 整数加法:将 236350 相加,得到 586
  3. 设置小数点位置:根据小数位数(这里是两位),将结果表示为 5.86

这个过程的核心在于,BigDecimal 把小数运算转换为了整数运算,而整数运算是不会丢失精度的。

下面是add方法的源码实现:

/**
 * Returns a BigDecimal whose value is (this + augend), 
 * and whose scale is max(this.scale(), augend.scale()).
 */
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.intVal, this.scale, augend.intVal, augend.scale);
        }
    }
}

进入第8行的add方法:

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);
        }
    }
}

这个例子中,该方法传入的参数分别是:xs=236,scale1=2,ys=35,scale2=1

该方法首先计算scale1 - scale2,根据差值走不同的计算逻辑,这里求出来是1,所以进入到最下面的else代码块(这块是关键):

  • 首先17行校验了一下数值范围
  • 18行将ys扩大了10的n次倍,这里n=raise=1,所以返回的scaledY=350
  • 接着就进入到20行的add方法:
private static BigDecimal add(long xs, long ys, int scale){
    long sum = add(xs, ys);
    if (sum!=INFLATED)
        return BigDecimal.valueOf(sum, scale);
    return new BigDecimal(BigInteger.valueOf(xs).add(ys), scale);
}

这个方法很简单,就是计算和,然后返回BigDecimal对象:

五、为什么 BigDecimal 不丢失精度?

现在我们已经明白了 BigDecimal 的基本原理,那么为什么它能够保证不丢失精度呢?原因就在于它把小数运算转换为了整数运算。整数运算是精确的,不会出现浮点数那种“四舍五入”的问题。

同时,BigDecimal 还提供了丰富的 API,支持各种数学运算,包括加法、减法、乘法、除法等。这些运算都基于整数运算,从而保证了精度。

举个例子,BigDecimal 的乘法运算会先将两个数的小数位数相加,然后进行整数乘法运算,最后根据总的小数位数设置小数点位置。这个过程同样保证了精度。

六、使用 BigDecimal 的注意事项

虽然 BigDecimal 是一个非常强大的工具,但在使用时也有一些需要注意的地方:

  1. 构造方法的选择:尽量使用字符串构造方法,而不是直接传入浮点数。因为浮点数本身就可能存在精度问题,而字符串构造方法可以精确地表示数值。

    BigDecimal bigDecimal = new BigDecimal("2.36"); // 推荐
    BigDecimal bigDecimal = BigDecimal.valueOf(2.36); // 不推荐
    
  2. 除法运算的精度BigDecimal 的除法运算可能会出现无限循环小数的情况,所以在进行除法运算时,需要指定精度和舍入模式。

    BigDecimal result = bigDecimal1.divide(bigDecimal2, 2, RoundingMode.HALF_UP);
    
  3. 性能问题:虽然 BigDecimal 能保证精度,但它的性能比 floatdouble 要差很多。所以在不需要高精度的场景下,尽量使用 floatdouble

七、BigDecimal 的实际应用

BigDecimal 在金融领域是不可或缺的工具。它虽然性能稍差,但精度极高,能够满足各种复杂的金融计算需求。比如,在银行系统中,账户余额、交易金额等都需要精确到小数点后两位,BigDecimal 是最佳选择。

此外,BigDecimal 还可以用于科学计算、大数据处理等场景。只要涉及到高精度的小数运算,BigDecimal 都能大显身手。

八、总结

今天,我们深入探讨了 BigDecimal 的原理和实现。通过把小数运算转换为整数运算,BigDecimal 能够精确地表示和计算小数,从而解决了浮点数精度丢失的问题。

在实际开发中,BigDecimal 是金融领域不可或缺的工具。它虽然性能稍差,但精度极高,能够满足各种复杂的金融计算需求。

最后,如果你觉得这篇文章对你有帮助,别忘了点赞和分享哦!

#后端##java#
全部评论

相关推荐

04-10 17:42
Java
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;一般情况下,国内绝大部分院校应届毕业生都会有一次三方违约的机会,以便毕业生可以签约心怡的意向单位,那么毕业生提出三方违约的流程是什么样子的?这里用10步为你详细介绍三方违约的流程,帮助你提前建立对三方违约的认知。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第一步,和原签约单位联系,表达三方违约意愿,按签约规定看是否需要赔偿违约金。一般情况下国企、银行、研究所违约金在1~2w之间,大厂一般没有违约金,部分私企违约金在5k~1w之间。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第二步,向原签约单位发送解约申请邮件,解约申请邮件模板一般原签约单位会提供,按照指定格式编写并发送邮件即可。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第三步,原签约单位收到解约申请邮件后,会向毕业生发送电子版解约函,注意:此解约函公司没有盖章,学校并不认可。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第四步,打印解约函并签字寄回原签约单位。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第五步,在规定期限内(一般是原签约单位回复解约申请邮件的7个自然日内)向原签约单位支付违约金。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第六步,原签约单位财务确认收款后,会将纸质版解约函盖章邮寄至毕业生处。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第七步,毕业生拿到纸质版解约函后将其扫描程电子版。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第八步,填写XX大学毕业生违约申请表,申请表填写完毕后打印下来,由学院领导签字盖章,之后扫描成电子版文件。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第九步,在网签系统上传电子版违约申请表、原签约单位解约函、接受单位录用通知书和违约申请理由,由毕业生所在学院审核。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第十步,学院审核通过后,交由原签约单位审核(原签约单位需要在网签系统点击违约同意按钮),通过后,学校审核,审核通过后,三方解约完毕。Tips:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.注意毕业生所在院校违约处理时间。有些院校需要在指定月份才开始处理违约,有些院校则没有时间限制。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.注意原签约单位处理解约时间。有些单位可随时解约,有些单位必须到第二年的三月份或四月份或五月份甚至有的七月份才处理违约申请。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.是否可以先签订两方。若原签约单位因某些因素无法处理解约,和接受单位是否可以先签订两方,等违约成功后再签订三方。注意:有的单位不接受两方签订!&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4.有的单位签订三方后不接受违约!这些单位不会出具解约函也不会在网签系统点击违约同意按钮,针对这些单位需要慎重考虑是否签约。
点赞 评论 收藏
分享
评论
5
8
分享

创作者周榜

更多
牛客网
牛客企业服务