注意了,这些数值计算的坑千万别踩!

作者 | 故里学Java  责编 | 张文

头图 | CSDN 下载自视觉中国

来源 | 故里学Java(ID:WLQ171223)

在我们日常工作中数值计算是不可避免的,特别是电商类系统中,这个问题一般情况下我们都是特别注意的,但是一不注意就会出大问题,跟钱有关的事情没小事。这不新来的大兄弟就一个不注意,在这个小阴沟里翻车了,闹笑话了。

为了我们以后可以避免在这个问题上犯错,我今天特地写了这一篇来总结一下。


避免用 Double 来运算

我们以为的算术运算和计算机计算的并不完全一致。

这是因为计算机是以二进制存储数值的,我们输入的十进制数值都会转换成二进制进行计算,十进制转二进制再转换成十进制就不是原来那个十进制了。

举个例子:十进制的0.1转换成二进制是0.0 0011 0011 0011...(无数个0011),再转换成十进制就是0.1000000000000000055511151231。

计算机无法精确地表达浮点数,这是不可避免的,也是为什么浮点数计算后精度损失的原因。

System.out.println(0.1+0.2);System.out.println(1.0-0.8);System.out.println(4.015*100);System.out.println(123.3/100);

通过简单的例子,我们发现精度损失并不是很大,但是这并不代表我们可以使用。特别是电商类系统中,每天几百万的单量,每笔订单哪怕少计算一分钱,最后算下来也是一笔不小的金额。

所以说,这不是个小事情。然后很多人就说,金额计算啊,你用 BigDecimal  啊。对的,这个没毛病,但是用了 BigDecimal 就万事大吉了吗?当问出这句话的时候,就说明这其中必有蹊跷。


BigDecimal 你遇见过哪些坑?

还是通过一个简单的例子,计算上边例子中的运算,来看一下结果:

System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));
System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));
System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));

我们发现使用了 BigDecimal 之后计算结果还是不精确,这里就要记住BigDecimal的第一个坑了:

BigDecimal 用来表示和计算浮点数的时候,要使用 String 的构造方法来初始化 BigDecimal。

改进一下再来看看结果:

System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));
System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100")));
System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));

那么接下来一个问题,使用了 BigDecimal 就万事大吉了吗?并不是的!

接下来我们来看一下 BigDecimal 的源码,这里面有一个地方需要注意,先看图:

注意看这两个属性,scale 表示小数点右边几位,precision 表示精度,就是我们常说的有效长度。

前边我们已经知道,BigDecimal 必须传入字符串类型数值,那么如果我们现在是一个 Double 类型数值,该如何操作呢?通过一个简单的测试我们来看一下:

 private static void testScale() {     BigDecimal bigDecimal1 = new BigDecimal(Double.toString(100));     BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));     BigDecimal bigDecimal3 = BigDecimal.valueOf(100d);     BigDecimal bigDecimal4 = new BigDecimal("100");     BigDecimal bigDecimal5 = new BigDecimal(String.valueOf(100));
     print(bigDecimal1);     print(bigDecimal2);     print(bigDecimal3);     print(bigDecimal4);     print(bigDecimal5);     }
private static void print(BigDecimal bigDecimal) {        System.out.println(String.format("scale %s precision %s result %s", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("1.001"))));}

运行后我们发现,以上前三种方式是将 double 转换成 BigDecimal 之后,得到的 BigDecimal 的 scale 都是1,precision都是4;后两种方式的  toString 方法得到的 scale 都是 0,precision 都是3,与 1.001 进行乘运算后,我们发现,scale 是两个数的 scale 相加的结果。

我们在处理浮点数的字符串的时候,应该显式的方式通过格式化表达式或者格式化工具来明确小数位数和舍入方式。


浮点数的舍入和格式化该如何选择?

我们首先来看看使用 String.format 的格式化舍入,会有什么结果,我们知道浮点数有 double 和 float 两种,下边我们就用这两种来举例子:

double num1 = 3.35;float num2 = 3.35f;System.out.println(String.format("%.1f", num1));System.out.println(String.format("%.1f", num2));

得到的结果似乎与我们的预期有出入。

其实这个问题很好解释,double 和 float 的精度是不同的。

double 的 3.35相当于3.35000……5625,而 float 的 3.35 相当于 3.34999……9375,String.format 才有的又是四舍五入的方式舍入,所以精度问题和舍入方式就导致了运算结果与我们预期不同。

Formatter 类中默认使用的是 HALF_UP 的舍入方式,如果我们需要使用其他的舍入方式来格式化,可以手动设置。

到这里我们就知道通过 String.format 的方式来格式化这条路坑有点多,所以,浮点数的字符串格式化还得要使用 BigDecimal 来进行。

来,上代码,测试一下究竟是不是那么回事:

BigDecimal num1 = new BigDecimal("3.35");//小数点后1位,向下舍入BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN);System.out.println(num2);//小数点后1位,四舍五入BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);System.out.println(num3);输入结果:3.33.4

这次得到的结果与我们预期一致。


BigDecimal 不能使用 equals 方法比较?

我们都知道,包装类的比较要使用 equals,而不能使用 ==。那么这一条在  Bigdecimal 中也适用吗?数据说话,简单的一个测试来说明:

System.out.println(new BigDecimal("1").equals(new BigDecimal("1.0")))结果:false

按照我们的理解 1 和 1.0 是相等的,也应该是相等的,但是 Bigdecimal 的 equals 在比较中不只是比较了 value,还比较了 scale。

我们前边说了 scale 是小数点后的位数,明显两个值的小数点后位数不一样,所以结果为 false。

实际的使用中,我们常常是只希望比较两个 BigDecimal 的 value,这里就要注意,要使用 compareTo 方法:

System.out.println(new BigDecimal("1").compareTo(new BigDecimal("1.0")))结果:true



最后

总结一下今天的文章:

  1. 避免使用 Double 来进行运算;

  2. BigDecimal 的初始化要使用 String 入参或者 BigDecimal.valueOf();

  3. 浮点数的格式化建议使用 BigDecimal;

  4. 比较两个 BigDecimal 的 value 要使用 compareTo。


更多精彩推荐
☞揭秘 AWS 基础架构底层运维和构建之道!

☞HarmonyOS 手机应用开发者 Beta 版到来,对开发者意味着什么

☞国内数据中心变革的见证者,揭秘阿里巴巴数据中心技术积淀

☞微软收购 GitHub 两年后,大咖共论开源新生态

☞红帽 与 CentOS 之间的恩怨情仇

☞清华硕士分享思维导图:机器学习所需的数学基础

点分享点点赞点在看
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值