"钱要用 BigDecimal,别用 double"是常见建议。真实验证 double 的精度误差长什么样,再验证一个更隐蔽的真实陷阱——用错构造函数的 BigDecimal 照样不精确。
上一篇:可变参数遇上重载:真实验证参数个数对得上时,编译器真实优先选固定版本
这是老生常谈的浮点数精度问题,真实跑一遍确认它确实会发生。
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,但用错构造函数,不精确的问题依然真实存在。
new BigDecimal(0.1) 真实结果 = 0.1000000000000000055511151231257827021181583404541015625 (0.1 这个 double 本身就不精确,BigDecimal(double) 构造函数真实原样继承了这份不精确)
真实结果和直接打印 new BigDecimal(0.1) 完全一致(小数点后一长串数字都对得上)——这不是巧合。BigDecimal(double) 这个构造函数的真实语义是"把这个 double 值在二进制层面的精确表示,原样转换成十进制"。传进去的 0.1 本身在 double 里就已经不精确了,这个构造函数忠实地把这份不精确"如实"翻译过来,没有做任何四舍五入或修正——它没有引入新的误差,但也完全没有修复已经存在的误差。
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 方法,连小数点后有几位 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 表示相等)
每一行都是真实运行这段代码得到的结果。