zoukankan      html  css  js  c++  java
  • 项目中业务代码使用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指定精度

    参考

  • 相关阅读:
    SpringMVC框架
    Spring框架
    Test_Shop项目开发练习
    MyBatis动态传参
    存储过程
    游标和触发器
    远程连接Linux系统管理
    安装Linux虚拟机
    request_html模块(下)
    request_html模块(上)
  • 原文地址:https://www.cnblogs.com/cdfive2018/p/14862515.html
Copyright © 2011-2022 走看看