在 Java 中隐藏着一个看似是 bug 的冷门现象:在一些数值计算中得不到你想象的结果,会多很多位小数点后面的数字。其实,这是浮点型数字的精度损失问题,本文简单做一个现象记录,以供读者参考。
在此说明,以下内容中涉及的代码已经被我上传至 Github:LossPrecision ,读者可以提前下载查看。
现象演示记录
假如读者按照下面的代码运行,猜猜看是什么结果。
1 | public void lossPrecisionTest () { |
请读者看到结果不要惊讶,是的,你没有看错,运行结果真的不是你想象的那样,总会多一点或者少一点。
1 | 2020-01-24_00:38:55 [main] INFO javabug.LossPrecision:15: ====sum:[0.060000000000000005] |

在 Java 中的简单浮点数类型 float 和 double,有时候不能够进行运算,其实不光是在 Java 中,在其它很多编程语言中也有这样的问题【本质在于硬件寄存器存储二进制数字会有精度损失】。尽管在大多数的情况下,计算的结果是准确的,但是有时候会出现意想不到的精度损失问题,读者也需要注意。
那么如何解决这个问题呢?【下面示例都以 4.015 这个数字演示】
简单四舍五入
我的第一个反应是做四舍五入【通过四舍五入把多余的数字尾巴清除掉,保留正确的数值】,Math 类中的 round 方法不能设置保留几位小数,只能像这样保留两位小数:
1 | // 1-Math 四舍五入 |
非常不幸,上面的代码并不能正常工作,得到的结果是错误的,给这个方法传入 4.015 它将返回 4.01 而不是 4.02,如我们在上面看到的:4.015 * 100 = 401.49999999999994。
得到结果:
1 | 2020-01-24_01:04:13 [main] INFO javabug.LossPrecision:25: ====round:[4.01] |

它只能保留 2 位小数,而且得到的结果还是错误的。
因此,如果我们需要做到精确的四舍五入,不能利用简单类型做任何运算,要想想其它方法。
数值格式化
那么这种问题还有没有其它办法呢?当然有,可以使用 DecimalFormat 格式化,代码如下:
1 | // 2-DecimalFormat 格式化,四舍五入,保留 2 位小数 |
运行后读者又发现,并没有得到想象的结果,仍旧是错误的,因为计算过程还是涉及到数值的精度损失问题。
1 | 2020-01-24_02:12:56 [main] INFO javabug.LossPrecision:33: ====format:[4.01] |

此时想必一些读者已经陷入了懵圈。
大数值计算
那么这种问题有没有其它办法可以彻底解决问题呢?当然有,可以使用 BigDecimal 计算。
其实,float 和 double 只能用来做 科学计算 或者是 工程计算 【允许损失一定的数值精度】,而在 商业计算 中我们要用 BigDecimal【不允许损失数值精度】,精度是可以保证的。
但是要注意,BigDecimal 有 2 种构造方法,一个是:BigDecimal (double val),另外一个是:BigDecimal (String val),请确保使用 String 来构造,否则在计算时还是会出现精度丢失问题,这算是 BigDecimal 的一个坑,很多人应该也遇到过。
代码示例:
1 | // 3-BigDecimal, 四舍五入,保留 2 位小数 |
运行结果:
1 | 2020-01-24_02:12:56 [main] INFO javabug.LossPrecision:37: ====multiply:[4.02] |

可以看到,使用 String 构造 BigDecimal 对象可以准确计算结果,而使用 double 构造 BigDecimal 对象还是会损失精度。
工具类
现在已经可以解决这个问题了,原则上是使用 BigDecimal 并且一定要用 String 来够造对象。
但是想像一下,如果我们要做一个加法运算,需要先将两个浮点数转为 String 类型,然后再构造成 BigDecimal 对象,在其中一个对象上调用 add 方法,传入另一个 BigDecimal 对象作为参数。然后把运算的结果,也是一个 BigDecimal 对象,再转换为浮点数。
我们能够忍受这么烦琐的过程吗?肯定不能,所以我在此提供一个工具类 BigDecimalUtil 来简化操作,它提供以下静态方法【参考下面的方法声明】,包括加减乘除和四舍五入,调用时可以传参从而灵活设置结果的精度和取舍的模式【四舍五入、去尾、进位等等】。
代码已经被我上传至 Github:BigDecimalUtil ,在这里就只贴出方法声明【类注释中可以看到】,读者可以自行下载使用。
1 | /** |
试运行结果如下:

备注
小问题
以下记录一个常见的判断差值为 0 的小问题。
如果在项目中碰到了如下的业务逻辑计算:
1 | double val1 = 61.5; |
结果发现这一组数据:61.5、60.4、1.1 无法达到正确的预期结果,即结果不为 0,有些人可能想破了脑袋也无法发现问题所在【千万不要试图拿计算器计算的结果对比,因为这是精度损失的问题】。
如果是有经验的开发人员一眼就可以发现问题所在,也知道应该采用如下的方式修改代码:
1 | // 加上允许精度损失的判断逻辑 |
这样的话,运行结果就会与期望一致了【同样的数值,只是更改了判断逻辑:允许精度损失】。

引申问题
除了精度损失的问题,还有一种 byte 类型自动转换的坑【当然,编译器有可能自动识别了问题代码,无法通过编译】。
有数值或者变量参与的加法运算,结果会转为 int 类型,再赋值给 byte 类型的变量无法通过编译,问题代码如下:
1 | // 类型自动转换问题 |

