bigywater.tech
Java 内部原理 · 44

BigDecimal 精度陷阱:真实验证 new BigDecimal(0.1) 会原样继承 double 的不精确

"钱要用 BigDecimal,别用 double"是常见建议。真实验证 double 的精度误差长什么样,再验证一个更隐蔽的真实陷阱——用错构造函数的 BigDecimal 照样不精确。

0.1+0.2 ≠ 0.3
真实计算:double 的 0.1+0.2 真实等于 0.30000000000000004,不等于 0.3
new BigDecimal(0.1) 依然不精确
真实验证:用 double 直接构造 BigDecimal,不精确会被真实原样继承过来,不会凭空变精确

上一篇:可变参数遇上重载:真实验证参数个数对得上时,编译器真实优先选固定版本

真实验证:double 的 0.1 + 0.2 不等于 0.3

这是老生常谈的浮点数精度问题,真实跑一遍确认它确实会发生。

真实实测
0.1 + 0.2 用 double 真实计算 = 0.30000000000000004
真实结果 == 0.3 = false

0.1 用 double 真实存储的完整精度 = 0.1000000000000000055511151231257827021181583404541015625

真实原因在最后一行就能看出来:double 用二进制浮点数表示,0.1 这个十进制小数在二进制里根本无法精确表示(就像 1/3 在十进制里是无限循环小数一样),double 存的 0.1 真实是一个极度接近但不完全等于 0.1 的近似值。两个近似值相加,误差被放大到了小数点后 17 位才能看见。

真实验证:BigDecimal(double) 构造函数的经典陷阱

很多人知道要用 BigDecimal,但用错构造函数,不精确的问题依然真实存在。

真实实测
new BigDecimal(0.1) 真实结果 = 0.1000000000000000055511151231257827021181583404541015625
(0.1 这个 double 本身就不精确,BigDecimal(double) 构造函数真实原样继承了这份不精确)

真实结果和直接打印 new BigDecimal(0.1) 完全一致(小数点后一长串数字都对得上)——这不是巧合。BigDecimal(double) 这个构造函数的真实语义是"把这个 double 值在二进制层面的精确表示,原样转换成十进制"。传进去的 0.1 本身在 double 里就已经不精确了,这个构造函数忠实地把这份不精确"如实"翻译过来,没有做任何四舍五入或修正——它没有引入新的误差,但也完全没有修复已经存在的误差。

真实验证:正确姿势,以及 0.1+0.2 真实精确等于 0.3

BigDecimal.valueOf(double) 和 new BigDecimal(String) 才是真实精确的构造方式。

真实实测
BigDecimal.valueOf(0.1) 真实结果 = 0.1
new BigDecimal("0.1") 真实结果 = 0.1
两者真实 equals = true

BigDecimal.valueOf(0.1).add(BigDecimal.valueOf(0.2)) 真实结果 = 0.3
真实 equals(new BigDecimal("0.3")) = true

BigDecimal.valueOf(double) 的真实实现是先把 double 转成字符串(Double.toString(),这一步会用最短的、能精确还原这个 double 值的十进制表示),再用字符串构造 BigDecimal——这条路径真实避开了"把 double 完整二进制精度原样转录"这个坑。用这种方式构造出来的 0.1 和 0.2 相加,真实精确等于 0.3,不再有任何误差。

真实验证:BigDecimal 的 equals 对小数位数敏感

另一个容易踩的坑:BigDecimal 重写的 equals 方法,连小数点后有几位 0 都会被算进去。

真实实测
new BigDecimal("1.0").equals(new BigDecimal("1.00")) = false(scale 不同,equals 真实判定不相等)
new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) = 0(compareTo 真实只比较数值,0 表示相等)
这意味着什么?数学上 1.0 和 1.00 显然是同一个数值,但 BigDecimal 真实把"数值"和"精度(scale)"当成两个独立的属性一起比较,equals() 要求两者都相同。真实、可靠的做法是:判断两个 BigDecimal 数值是否相等,该用 compareTo() == 0,不是 equals()——这也是很多"BigDecimal 用在 Set/Map 里却出现诡异重复"问题的真实根源,因为 HashSet/HashMap 默认走的正是 equals() + hashCode() 这条路径。

分步演示:BigDecimal 的"精确"到底精确在哪一步

每一行都是真实运行这段代码得到的结果。

BigDecimal 的精确性真实取决于是用字符串还是 double 构造;equals() 和 compareTo() 对 scale 的处理真实不一致
这一步新验证的运算

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到。
    • 和int 溢出那篇一样,都是"数值类型的静默失真,没有任何异常提示"这一类真实陷阱。
    • 没有涉及:BigDecimal 除法可能真实抛出 ArithmeticException(除不尽又没指定舍入模式)、不同 RoundingMode 之间的真实数值差异——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号