数值计算在我们的日常工作中是不可避免的,尤其是在电子商务系统中。平时我们会特别注意这个问题,但是如果不注意,就会出大问题。跟钱有关的事非同小可。老大哥,不是新来的,一不留神,就把车翻在这个小阴沟里,闹出了笑话。为了避免我们以后在这个问题上犯错误,今天特地写了这篇文章来总结一下。公众号:Java架构师联盟,每日更新技术文章避免使用Double计算,使用Double计算。我们认为算术运算和计算机计算不完全一样,因为计算机是以二进制存储值的。我们输入的十进制值会被转换成二进制进行计算。十进制转二进制再转十进制就不是原来的十进制了,也不再是以前的那个少年了。比如:0.1在十进制转换成二进制就是0.0001100110011...(无数个0011),再转换成十进制就是0.1000000000000000055511151231,你看,我没骗你。计算机无法精确表达浮点数是不可避免的,这就是浮点计算后精度丢失的原因。为此,我做了一个小演示publicclasstest{publicstaticvoidmain(String[]args){System.out.println(0.1+0.2);System.out.println(1.0-0.8);系统输出。println(4.015*100);System.out.println(123.3/100);}}通过一个简单的例子,我们发现精度损失不是很大,但这并不代表我们可以用,尤其是电商系统,其中如果说每天少几百万单,就算每单少算一分钱,也不是小数目。所以,这可不是一件小事,很多人都说,量是算出来的,你用BigDecimal,没错,这个没有错,但是你用BigDecimal就可以了吗?这句话问出来,就说明这里面一定有什么奇怪的地方。您在使用BigDecimal时遇到过哪些陷阱?或者通过一个简单的例子,计算上面例子中的运算,看看结果:publicclasstest{publicstaticvoidmain(String[]args){System.out.println(newBigDecimal(0.1).add(新的BigDecimal(0.2)));System.out.println(newBigDecimal(1.0).subtract(newBigDecimal(0.8)));System.out.println(newBigDecimal(4.015).multiply(newBigDecimal(100)));System.out.println(newBigDecimal(123.3).divide(newBigDecimal(100)));}}看看结果偏离了多少G:JDK1.8binjava.exe"-javaagent:G:ideaIntelliJIDEA2019.3.3libidea_rt.jar=14053:G:ideaIntelliJIDEA2019.3.3bin"-Dfile.encoding=UTF-8-类路径G:JDK1.8jrelibcharsets.jar;G:JDK1.8jrelibdeploy.jar;G:JDK1.8jrelibextaccess-bridge-64.jar;G:JDK1.8jrelibextcldrdata.jar;G:JDK1.8jrelibextdnsns.jar;G:JDK1.8jrelibextjaccess.jar;G:JDK1.8jrelibextjfxrt.jar;G:JDK1.8jrelibextslocaledata.jar;G:JDK1.8jrelibextnashorn.jar;G:JDK1.8jrelibextsunec.jar;G:JDK1.8jrelibextsunjce_provider.jar;G:JDK1.8jrelibextnashorn.jar;G:JDK1.8jrelibextsunpkcs11.jar;G:JDK1.8jrelibextzipfs.jar;G:JDK1.8jrelibjavaws.jar;G:JDK1.8jrelibjce.jar;G:JDK1.8jrelibjfr.jar;G:JDK1.8jrelibjfxswt.jar;G:JDK1.8jrelibjsse.jar;G:JDK1.8jrelibmanagement-agent.jar;G:JDK1.8jrelibplugin.jar;G:JDK1.8jrelibresources.jar;G:JDK1.8jrelibrt.jar;E:Javaxtesttargetclasses;D:MAVENmvn_repoorgapacherocketmqrocketmq-client4.6.1rocketmq-client-4.6.1.jar;D:MAVENmvn_repoorgapachecommonscommons-lang33.4commons-lang3-3.4.jar;D:MAVENmvn_repoorgapacherocketmqrocketmq-common4.6.1rocketmq-common-4.6.1.jar;D:MAVENmvn_repoorgapacherocketmqrocketmq-remoting4.6.1rocketmq-remoting-4.6.1.jar;D:MAVENmvn_repocomalibabafastjson1.2.61fastjson-1.2.61.jar;D:MAVENmvn_repoionettynetty-all4.0.42.Finalnetty-all-4.0.42.Final.jar;D:MAVENmvn_repoorgapacherocketmqrocketmq-logging4.6.1rocketmq-logging-4.6.1.jar;D:MAVENmvn_repoionettynetty-tcnative-boringssl-static1.1.33.Fork26netty-tcnative-boringssl-static-1.1.33.Fork26.jar;D:MAVENmvn_repocommons-validatorcommons-validator1.6commons-validator-1.6.jar;D:MAVENmvn_repocommons-beanutilscommons-beanutils1.9.2commons-beanutils-1.9.2.jar;D:MAVENmvn_repocommons-digestercommons-digester1.8.1commons-digester-1.8.1.jar;D:MAVENmvn_repocommons-logglogging1.2commons-logging-1.2.jar;D:MAVENmvn_repocommons-collectionscommons-collections3.2.2commons-collections-3.2.2.jarcom.biws.test?0.30000000000000001665334536937734810635447502136230468750.1999999999999999555910790149937383830547332763671875401.499999999999968025576890795491635799407958984375001.232999999999999971578290569595992565155029296875?Processfinishedwithexitcode0我们发现使用了AfterBigDecimal,计算结果还是不准确。这里一定要记住BigDecimal的第一个坑:当用BigDecimal表示和计算浮点数时,要用String的构造函数来初始化BigDecimal做一个小改进,让我们看看结果:publicclasstest{publicstaticvoidmain(String[]args){System.out.println(newBigDecimal("0.1").add(newBigDecimal("0.2")));System.out.println(newBigDecimal("1.0").subtract(newBigDecimal("0.8")));System.out.println(newBigDecimal("4.015").multiply(newBigDecimal("100")));System.out.println(newBigDecimal("123.3").divide(newBigDecimal("100")));}}那么下一个问题,BigDecimal就万事大吉了吗?并不真地!接下来我们看一下BigDecimal的源码。有一个地方需要注意。先看图:privatefinalintscale;//注意:这可能有任何值,因此//计算必须以longs形式完成/**此BigDecimal中的小数位数,如果位数未知(后备信息),则为0。如果非零,则该值保证正确。如果该值可能为0,则使用precision()方法获取并设置该值。该字段在设置为非零之前是可变的。*@since1.5*/privatetransientintprecision;注意这两个属性,scale表示小数点右边的位数,precision表示精度,也就是我们常说的有效长度。我们已经知道BigDecimal必须传入一个string类型的值,那么如果我们现在是一个Double类型的值,应该如何操作呢?让我们来看一个简单的测试:publicclasstest2{privatestaticvoidtestScale(){BigDecimalbigDecimal1=newBigDecimal(Double.toString(100));BigDecimalbigDecimal2=newBigDecimal(String.valueOf(100d));BigDecimalbigDecimal3=BigDecimal.valueOf(100d);BigDecimalbigDecimal4=newBigDecimal("100");BigDecimalbigDecimal5=newBigDecimal(String.valueOf(100));print(bigDecimal1);打印(bigDecimal2);打印(bigDecimal3);打印(bigDecimal4);打印(bigDecimal5);}privatestaticvoidprint(BigDecimalbigDecimal){System.out.println(String.format("scale%sprecision%sresult%s",bigDecimal.scale(),bigDecimal.precision(),bigDecimal.multiply(newBigDecimal("1.001"))));}publicstaticvoidmain(String[]args){testScale();}}运行我们发现,上面的前三个方法都是将double转为BigDecimal后,得到的BigDecimal的scale为1,精度为4。后两种方法的toString方法得到的scale既是0又是a精度为3。乘以1.001后,我们发现scale是两个数的scale相加的结果。我们在处理浮点型字符串时,应该通过格式化表达式或格式化工具明确指定小数点后的位数和舍入方式。如何选择浮点数的舍入和格式?我们先来看一下使用String.format格式化后的舍入结果是什么,我们知道浮点数有两种类型,double和float,下面就以这两种为例:publicclasstest3{publicstaticvoidmain(String[]args){doublenum1=3.35;浮点数2=3.35f;System.out.println(String.format("%.1f",num1));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来完成。来,上代码,测试下是不是这样://改变格式BigDecimalnum3=newBigDecimal("3.35");//一位小数,向下取整BigDecimalnum4=num3.setScale(1,BigDecimal.ROUND_DOWN);System.out.println(num2);//小数点后1位,四舍五入BigDecimalnum5=num3.setScale(1,BigDecimal.ROUND_HALF_UP);System.out.println(num3);这次得到的结果和我们预想的一样。BigDecimal不能使用equals方法进行比较?我们都知道包装类的比较应该使用equals而不是==,那么这在Bigdecimal中是否也适用?数据说话,一个简单的测试来说明:System.out.println(newBigDecimal("1").equals(newBigDecimal("1.0")))Result:false复制代码根据我们的理解,1和1.0是相等的,应该也是相等的,但是Bigdecimal的equals在比较的时候不仅是比较值,还会比较小数位。我们前面说过,刻度是小数点后的位数。很明显,两个值的小数点后位数不同,所以结果为假。在实际使用中,我们往往只想比较两个BigDecimals的值。这里要注意compareTo方法:System.out.println(newBigDecimal("1").compareTo(newBigDecimal("1.0")))Result:true最后总结一下今天的文章:AvoidusingDoubletoperform计算。初始化BigDecimal以使用String输入参数或BigDecimal.valueOf()来格式化浮点数。比较两个BigDecimals的值推荐使用BigDecimal。
