浮点数精确计算——BigDecimal

本文探讨了double和float不精确的原因,由于二进制表示的局限性导致的精度损失。接着介绍了BigDecimal如何实现精确计数,通过无标度值和标度表示数,并强调了使用String构造方法或valueOf()的必要性。最后,讨论了使用BigDecimal时的注意事项,如避免使用equals方法比较大小,推荐使用compareTo进行比较。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、为什么 double(或 float) 不精确?

  • 计算机只认识二进制
  • 十进制小数转换成二进制,一般采用“乘二取整,顺序排列”的方法,但有的小数并不能转换成二进制,如 0.1 就不能直接用二进制表示。它的二进制是0.000110011001100……,这是一个无限循环小数;
  • 为了解决不能转换成二进制的问题,人们想出了一种采用一定的精度,使用近似值表示一个小数的方法:IEEE 754(IEEE 二进制浮点数算数标准)规范的主要思想(IEEE 754 规定了多种表示浮点数值的方式,其中最常用的就是 32 位单精度浮点数 和 64 位双精度浮点数);
  • 所谓精度不同,可以简单理解为保留有效位数不同。

2、BigDecimal 如何精确计数?

  • 看源码就能发现:一个 BigDecimal 实际上是通过一个“无标度值”和一个“标度”来表示一个数的;
【scale】123.45:2
【unscaledValue】123.45:12345
【scale】-3000:0
【unscaledValue】-3000:-3000
* 标度通过 scale 字段来表示
* 无标度值的表示比较复杂
    * 当 unscaled value 超过阈值(默认为 Long.MAX_VALUE)时采用 intVal 字段存储 unscaled value,intCompact 字段存储 Long.MIN_VALUE;
    * 否则对 unscaled value 进行压缩存储到 long 型的 intCompact 字段用于后续计算,intVal 为空。
    * 涉及到的字段如下:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
      
    private final BigInteger intVal;
    // 表示标度
    private final int scale; 

    private final transient long intCompact;
}
  • 标度到底是什么?
    • 除了 scale 字段,还提供了 scale() 方法,用来返回这个 BigDecimal 的标度。
    • 如果scale为零或正值,则该值表示这个数字小数点右侧的位数;
    • 如果scale为负数,则该数字的真实值需要乘以10的 该负数的绝对值 的幂。例如,scale为-3,则这个数需要乘1000,即在末尾有3个0。
    • eg:二进制无法表示的 0.1,使用 BigDecimal 就可以表示了,即通过无标度值 1 和标度值 1 来表示。
  • BigDecimal 有多个构造方法(JDK 1.8 一个参数的有5个,多了一个 (char[] in)),如下四个不同的构造方法创建出来的 BigDecimal 对象的标度(scale)是不同的。
    • BigDecimal(int):比较简单,因为是整数,所以他们的标度都是零
    • BigDecimal(double):
    • BigDecimal(long):比较简单,因为是整数,所以他们的标度都是零
    • BigDecimal(String):

3、使用注意事项

3.1 创建 BigDecimal 对象使用的方法

  • ✅ 优先推荐入参为 String 的构造方法;
  • ✅ 或使用 BigDecimal 的 valueOf 方法(该方法内部其实执行了 Double 的 toString 方法,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断)。
  • ❌ 禁止使用构造方法 BigDecimal(double) 的方式把 double 转化为 BigDecimal 对象(存在精度损失风险)
  • eg:要正确创建一个能精确表示 0.1 的 BigDecimal,请使用以下两种方式:
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);

3.2 ❌ BigDecimal(double)有什么问题?

  • 使用 BigDecimal(double) 创建的 BigDecimal 对象,有可能是损失了精度的,这是极其严重的。
  • 因为 double 表示的小数是不精确的,如 0.1 这个数字,double 只能表示它的近似值。所以,当使用 new BigDecimal(0.1) 创建一个 BigDecimal 对象时,其实创还能出来的值并不是正好等于 0.1 的。
    BigDecimal(double)有什么问题?

3.3 ❌ 禁止使用 equals 方法比较大小

  • 为什么?
    • equals 的原理(源码):如果标度值不相等就直接返回不相等的结果;
      在这里插入图片描述
    • BigDecimal 的表示原理,相同的数标度值可能不同。因为 BigDecimal 是用一个标度值和一个非标度值组合起来表示一个数的,而不同的构造方法和参数会有不同的标度值。比如:使用“new BigDecimal(“0.1”)” 和 “new BigDecimal(“0.100”)”创建的对象的标度值不同,分别是 1 和 3。

3.4 ✅ 使用 compareTo 比较大小

  • 源码如下:
public int compareTo(BigDecimal val) {
    // Quick path for equal scale and non-inflated case.
    if (scale == val.scale) {
        long xs = intCompact;
        long ys = val.intCompact;
        if (xs != INFLATED && ys != INFLATED)
            return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
    }
    int xsign = this.signum();
    int ysign = val.signum();
    if (xsign != ysign)
        return (xsign > ysign) ? 1 : -1;
    if (xsign == 0)
        return 0;
    int cmp = compareMagnitude(val);
    return (xsign > 0) ? cmp : -cmp;
}
  • 示例:
BigDecimal bigDecimal1 = new BigDecimal("0.1");
BigDecimal bigDecimal2 = new BigDecimal("0.100");
System.out.println("【scale】" + bigDecimal1 + ":" + bigDecimal1.scale());
System.out.println("【scale】" + bigDecimal2 + ":" + bigDecimal2.scale());
System.out.println("【equals】:" + bigDecimal1.equals(bigDecimal2));
System.out.println("【compareTo】:" + bigDecimal1.compareTo(bigDecimal2));

结果:

【scale】0.11
【scale】0.1003
【equals】:false
【compareTo】:0

3.5 为什么 BigDecimal.valueOf() 能保证精确(思考题)?

  • BigDecimal.valueOf() 是调用 Double.toString() 方法实现的;
    • -问: 既然 double 都是不精确的,BigDecimal.valueOf(0.1) 是如何保证精确的呢?
    • -答: Double.toString() 方法 的结果是所传参数的精确值的 String 值(所以,问题在于 Double.toString() 的返回结果为什么能保证精度,然后问题看起来倒像是有些误导了)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值