背景
今天收到产品反馈一个线上问题,运营在设置组合商品价格时,输入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指定精度
参考
- java float double精度为什么会丢失?浅谈java的浮点数精度问题 https://blog.csdn.net/abing37/article/details/5332798
- java面试官:Double为什么会丢失精度?解决方法?答出给1万月薪 https://cloud.tencent.com/developer/article/1468551
- java中double数据精度丢失问题 https://www.jianshu.com/p/e3652382093b
- 十进制的0.1 为什么不能用二进制很好的表示? https://blog.csdn.net/Lixuanshengchao/article/details/82049191