项目中业务代码使用Double类型字段计算导致丢失精度问题

背景

今天收到产品反馈一个线上问题,运营在设置组合商品价格时,输入19.9点击保存后变成了18.9。

分析

这个功能3年前就有了,当时还没来公司,第一次收到反馈这样的问题。
定位到该接口,注意到接口的请求vo是用Double类型定义的价格相关字段,

类似:

public class XxxReqVo implements Serializable {
    ...
    // 市场价
    private Double marketPrice;
    // 销售价
    private Double salePrice;
}

在测试环境复现后,通过F12开发者控制台看,发现前端的传值是没问题,传值是19.9。
查看接口代码,内部有如下处理,把前端传的价格元转换成了分:

Double fenPrice = reqVo.getPrice() * 100;
Double fenMarketPrice = Optional.fromNullable(reqVo.getMarketPrice()).or(reqVo.getPrice()) * 100;

接着转成String调用了另一个方法:

setXxxPrice(product, String.valueOf(fenPrice.intValue()), String.valueOf(fenMarketPrice.intValue()))

setXxxPrice方法里有业务上的处理,保存至Redis的逻辑将String转成Integer,推送给中台价格中心的逻辑将分又转成了元。
想到Double类型有精度类型丢失问题,注意到这句:
Double fenPrice = reqVo.getPrice() * 100;当前端参数为19.9时,乘以100后Double类型的fenPrice变量可能丢失了精度。

写个程序验证如下:

System.out.println(19.9); // 19.9
System.out.println(19.9D); // 19.9
System.out.println(19.9 * 100); // 1989.9999999999998
System.out.println(19.9D * 100); // 1989.9999999999998

精度果然丢失了,在转成Integer后就变成了1989,单位是分,转成元就是19.89,跟反馈的问题一致。
换几个价格试试:

System.out.println(20.9 * 100); // 2090.0
System.out.println(9.9 * 100); // 999.0

这几个没有问题,结果符合预期。
想起之前遇到过的场景:

System.out.println(0.1 * 3); // 0.30000000000000004

Double类型数值在做运算时可能会丢失精度是个很常见的开发注意点,看来在当时接口完成后没有做好代码Review。
该接口代码已有3年历史了,当时开发的同学已经不在公司也没有见过。

解决

于是我对代码进行了修正,改为用BigDecimal类型来进行运算,前端参数类型不变,优化内部方法多余的类型转换。

Integer fenPrice = new BigDecimal(String.valueOf(reqVo.getPrice())).multiply(new BigDecimal("100")).intValue();
nteger fenMarketPrice = reqVo.getMarketPrice() != null ? (new BigDecimal(String.valueOf(reqVo.getMarketPrice())).multiply(new BigDecimal("100")).intValue()) : fenPrice;
...
setXxxPrice(product, fenPrice, fenMarketPrice)

这里使用BigDecimal传字符串的构造方法,用multiply方法做乘法运算,考虑到是乘100,通过intValue方法然后自动装箱转为Integer类型。

还有其它方法可以实现,写个小程序测试下:

System.out.println(new BigDecimal("19.9").multiply(new BigDecimal("100"))); // 1990.0
System.out.println(new BigDecimal("19.9").multiply(new BigDecimal("100")).setScale(0)); // 1990
System.out.println(new BigDecimal("19.9").multiply(new BigDecimal("100")).stripTrailingZeros()); // 1.99E+3
System.out.println(new BigDecimal("19.9").multiply(new BigDecimal("100")).stripTrailingZeros().toPlainString()); // 1990
System.out.println(new BigDecimal("19.9").multiply(new BigDecimal("100")).toBigInteger()); // 1990
System.out.println(new BigDecimal("19.9").multiply(new BigDecimal("100")).toBigInteger().intValue()); // 1990
System.out.println(new BigDecimal("19.9").multiply(new BigDecimal("100")).intValue()); // 1990

总结

  • Double类型计算和转换时可能有精度丢失问题
  • 项目开发中在进行金额计算时通常用BigDecimal或Integer类型
  • Integer转换为分计算加、减、乘没有小数位,但要注意溢出问题
  • BigDecimal应使用字符串参数的构造方法,注意四舍五入方法以及setScale指定精度

参考

原文地址:https://www.cnblogs.com/cdfive2018/p/14862515.html