文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

刚来的大兄弟在这个小问题上翻车了,你确定不看一下?

2024-12-03 15:22

关注

本文转载自微信公众号「 故里学Java」,作者 故里学Java。转载本文请联系 故里学Java公众号。

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

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

避免用Double来进行运算

使用Double来计算,我们以为的算术运算和计算机计算的并不完全一直,这是因为计算机是以二进制存储数值的,我们输入的十进制数值都会转换成二进制进行计算,十进制转二进制再转换成十进制就不是原来那个十进制了,再也不是曾经那个少年了。举个例子:十进制的0.1转换成二进制是0.0 0011 0011 0011...(无数个0011),再转换成十进制就是0.1000000000000000055511151231,看到了吧,没有骗你的。

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

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

通过简单的例子,我们发现精度损失并不是很大,但是这并不代表我们可以使用,特别是电商类系统中,每天少说几百万的单量,每笔订单哪怕少计算一分钱,算下来也是一笔不小的金额,所以说,这不是个小事情,然后很多人就说,金额计算啊,你用BigDecimal啊,对的,这个没毛病,但是用了BigDecimal就完事大吉了吗?当问出这句话的时候,就说明这其中必有蹊跷。

BigDecimal你遇见过哪些坑?

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

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

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

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

小的改进一下再来看看结果:

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

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

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

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

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

  1.  private static void testScale() { 
  2.      BigDecimal bigDecimal1 = new BigDecimal(Double.toString(100)); 
  3.      BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d)); 
  4.      BigDecimal bigDecimal3 = BigDecimal.valueOf(100d); 
  5.      BigDecimal bigDecimal4 = new BigDecimal("100"); 
  6.      BigDecimal bigDecimal5 = new BigDecimal(String.valueOf(100)); 
  7.  
  8.      print(bigDecimal1); 
  9.      print(bigDecimal2); 
  10.      print(bigDecimal3); 
  11.      print(bigDecimal4); 
  12.      print(bigDecimal5);      
  13.  
  14. private static void print(BigDecimal bigDecimal) { 
  15.         System.out.println(String.format("scale %s precision %s result %s", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("1.001")))); 

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

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

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

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

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

得到的结果似乎与我们的预期有出入,其实这个问题也很好解释,double和float的精度是不同的,double的3.35相当于3.350000000000000088817841970012523233890533447265625,而float的3.35相当于3.349999904632568359375,String.format才有的又是四舍五入的方式舍入,所以精度问题和舍入方式就导致了运算结果与我们预期不同。

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

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

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

  1. BigDecimal num1 = new BigDecimal("3.35"); 
  2. //小数点后1位,向下舍入 
  3. BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN); 
  4. System.out.println(num2); 
  5. //小数点后1位,四舍五入 
  6. BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP); 
  7. System.out.println(num3); 
  8. 输入结果: 
  9. 3.3 
  10. 3.4 

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

BigDecimal不能使用equals方法比较?

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

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

按照我们的理解1和1.0是相等的,也应该是相等的,但是Bigdecimal的equals在比较中不只是比较了value,还比较了scale,我们前边说了scale是小数点后的位数,明显两个值的小数点后位数不一样,所以结果为false。

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

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

最后

再总结一下今天的文章:

 

来源: 故里学Java内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯