文章主要讲述测试人员在测试过程中发现的一个bug,即订单详情中的价格展示不准确,经过排查发现是double类型的精度问题导致的。文章通过实例详细解释了double类型在进行金额转换时存在的精度问题,并提供了使用BigDecimal类进行金额转换的方法。同时,文章还进行了性能测试,发现使用BigDecimal类的性能影响并不大。
文章中提到,测试人员发现了一个bug,订单详情中的价格展示不正确。经过排查,确定是double类型的精度问题导致的。
文章通过实例详细解释了double类型在进行金额转换时,由于计算机中的二进制补码存储方式,存在除不尽、精度丢失的问题。例如,2.01在二进制补码存储时会变成000000010.009999999999999787。
文章介绍了Java中的BigDecimal类,专门用于处理更高精度的浮点数。文章提供了将元和分互相转换的方法,并进行了测试,证明其精度和性能都是可靠的。
文章中进行了性能测试,测试结果显示,使用BigDecimal类进行金额转换的性能影响并不大。虽然首次耗时略高,但整体性能良好。
前些日子,测试提过来一个bug,说下单价格应该是 2.01,但是在订单详情中展示了2.00元。我头嗡的一下子,艹,不会是因为double 的精度问题吧~
果不其然,经过排查代码,最终定位原因订单详情展示金额时,使用double 进行了金额转换,导致金额不准。
我马上排查核心购买和售后链路,发现涉及资金交易的地方没有问题,只有这一处问题,要不然这一口大锅非得扣我身上。
为什么 2.01 变成了 2.0
2.01等小数 在计算机中按照2进制补码存储时,存在除不尽,精度丢失的问题。
例如 2.01的补码为
000000010.009999999999999787
。正如十进制场景存在 1/3等无限小数问题,二进制场景也存在无限小数,所以一定会存在精度问题。
什么场景小数转换存在问题
for (int money = 0; money 10000; money++) {
String valueYuan = String.format("%.2f", money * 1.0 / 100);
int value = (int) (Double.valueOf(valueYuan) * 100);
if (value != money) {
System.out.println(String.format("原值: %s, 现值:%s", money, value));
}
}
如上代码中,先将数字 除以 100,转为元, 精度为2位,然后将double 乘以100,转为int。在除以、乘以两个操作后,精度出现丢失。
我把1-10000 的范围测试一遍,共有573个数字出现精度转换错误。这个概率已经相当大了。
如何转换金额更安全?
Java 提供了
BigDecimcal
专门处理精度更高的浮点数。简单封装一下代码,元使用String表示,分使用int表示。提供两个方法实现 元和分的 互转。
public static String change2Yuan(int money) {
BigDecimal base = BigDecimal.valueOf(money);
BigDecimal yuanBase = base.divide(new BigDecimal(100));
return yuanBase.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
}
public static int change2Fen(String money) {
BigDecimal base = new BigDecimal(money);
BigDecimal fenBase = base.multiply(new BigDecimal(100));
return fenBase.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
}
unset
unset
测试
unset
unset
测试0-1 亿 的金额转换逻辑,均成功转换,不存在精度丢失。
int error = 0;
long time = System.currentTimeMillis();
for (int money = 0; money 100000000; money++) {
String valueYuan = change2Yuan(money);
int value = change2Fen(valueYuan);
if (value != money) {
error++;
}
}
System.out.println(String.format("时间:%s", (System.currentTimeMillis() - time)));
System.out.println(error);
unset
unset
性能测试
unset
unset
网上很多人说使用 BigDecimcal 存在性能影响,但是我测试性能还是不错的。可能首次耗时略高,大约2ms
总结
涉及金额转换的 地方,一定要小心处理,防止出现精度丢失问题。可以使用代码审查工具,查看代码中是否存在使用double 进行金额转换的代码, 同时提供 金额转换工具类。
来源:
juejin.cn/post/7399985723673837577