bigdecimal计算开n次方_数值计算:你与首富的距离可能就差这“一点”

      数值计算在软件开发过程中可以说是基本操作了,而涉及数值计算的业务往往不是真金白银,就是工程科学,一旦在精度上出现问题,后果可能要比rm -rf要来得致命。       我第一次遇到精度计算问题大概是在2011年,那会在浙江中烟,项目经理正眉飞色舞地给财务处长演示预算管理,这里面有个汇总表,能把所有预算科目下的预算数作展示。然而没过几分钟,财务处长就直接指出来:你这预算数加起来的总额不对啊!于是我们认真地打开电脑的计算器人肉计算了下,还真是错的。虽然误差不大,就差了个零点几,但对于业务而言,这个显然是不允许的。后来排查代码的时候发现,后端代码对于金额变量,全是用了Double类型!由于事出紧急,就直接在现场撸代码了,把Double统统换成BigDecimal,问题得到解决。

危险的double

那么,先来考大家一个小学计算题吧:

1.1 - 0.6 = ?
0.6 + 0.3 = ?

答案很明显是0.5和0.9,这个估计不会有人算错。那么我们来试试用程序来计算一下,会不会得到我们预期的结果呢。我们来看下下面这段简单代码的执行结果:

public static void main(String[] args) {    double a = 1.1d;    double b = 0.6d;    double c = 0.3d;    System.out.println("1.1 - 0.6 = " + (a - b));    System.out.println("0.6 + 0.3 = " + (b + c));}

对应的执行结果是这样的:

1.1 - 0.6 = 0.5000000000000001
0.6 + 0.3 = 0.8999999999999999

完了,图灵的棺材板按不住了。计算机连区区小学生的题目都算错,而且不止减法,加法都能算错。也许有同学不服气,觉得这可能是Java的Bug。于是我们来试试用C语言实现同样的逻辑看看结果会是怎么样的:

#includeint main(){    double a = 1.1;    double b = 0.6;    double c = 0.3;    printf("1.1 - 0.6 = %0.16f\n", a - b);    printf("0.6 + 0.3 = %0.16f\n", b + c);    return 0;}

然后遗憾的是,执行结果是这样子的:

1.1 - 0.6 = 0.5000000000000001
0.6 + 0.3 = 0.8999999999999999

可以看到与Java的执行结果是一致的。

      其实在计算机内部,使用的都是二进制数,所以我们的十进制数在进入运算前是会被转换成二进制数的,于是,1.1会被转成1.0001100110011001100...(1100无限循环),而0.6则是0.10011001100110011001...(1001无限循环),而CPU寄存器的位数是有限的,所以在运算过程中就出现了精度上的损失。所以对于计算机而言,许多小数无法精确表达,这才是精度丢失的根源。

      有些同学可能会觉得,结果已经非常接近了,不会产生什么影响,特别对于前端页面,可能就一个toFixed(n)解决问题。可是对于金融计算领域,这种微小的误差不断累积,将被不断放大,最终结果就是老板发现,今年帐上无故丢了几十万。

BigDecimal有时候也不那么靠谱

      于是在职业生涯中一定会有人教育你,金额计算,请丢弃Double,使用BigDecimal,浮点数精确表达和运算的场景,一定要使用这个类型。然而……
我们再来看个例子:

System.out.println(new BigDecimal(0.6).add(new BigDecimal(0.3)));System.out.println(new BigDecimal(1.1).subtract(new BigDecimal(0.6)));

输出结果是这样的:

0.899999999999999966693309261245303787291049957275390625
0.50000000000000011102230246251565404236316680908203125

这……除了精度高了,跟Double是有毛线的区别了,还是不准确啊。所以使用BigDecimal的一个注意点,实例化初始值,不要使用浮点数,而是使用字符串来初始化。为了图省事,实体类上用的浮点,只在计算上使用了BigDecimal,是达不到预期效果的,这是个妥妥的坑点。

      其实在这个过程中,为了省事,我还试过了另外一种方案,可能聪明的小伙伴们已经想到了,用Double.toString()直接转成字符串来初始化不就完事了。确实,这个方案在大部分情况下是很受用的,只是在特定情况下,会引发一些不必要的麻烦,比如下面这个例子:

System.out.println(new BigDecimal("3.14159").multiply(new BigDecimal(100)));System.out.println(new BigDecimal("3.14159").multiply(new BigDecimal(Double.toString(100))));

它的运行结果其实是酱紫的:

314.15900
314.159000

我们发现,使用Double.toString()出来的,计算后诡异地多了一位精度!

      其实这个问题是由于BigDecimal乘法时计算scale的方式是将乘数与被乘数的scale相加,而问题就出在,使用字符串实例化出来的scale是0,而使用Double.toString()出来的却是1,于是算出来的两个结果的精度刚好相差1。所以你就不得不为你的计算结果做格式化了。

      第二个BigDecimal的问题是判等。这里说的判等不是指两数是否相等的compareTo()方法,而是equals()方法。这个问题比较小众,大部分童鞋不一定有机会踩到。假设有这么一个场景,我们有个HashSet,然后要把BigDecimal的值丢进去,这时候让Set帮我们去重。但是结果却无法令我们满意。来看下面这个例子:

Set hs = new HashSet<>();hs.add(new BigDecimal("2.0"));hs.add(new BigDecimal("2.00"));System.out.println(hs.size());

得到的结果不是1,而是2。

      2.0与2.00被认为是不同的两个数,这在大部分业务上是不能被接受的。简单地翻看源码就会知道,BigDecimal的equals不仅判断了value,还判断了scale。所以就像注释上说的,2.0跟2.00其实是不等的。

29972baecc3345e262b24340bcd771a0.png

对于上述的场景,解决方案有两种:

  • 使用TreeSet来替换HashSet, 因为TreeSet用的是compareTo来比较的

  • 调用BigDecimal的stripTrailingZeros()方法,把零截断,这样就保证了scale是一样的了。

溢出

      对于计算机而言,溢出是常规操作。对于一个数字,人脑要灵活多了,不会因为位数过多而出现认知问题,但CPU就不一样了,它最终要依赖电路,以0和1的这种二进制方式来识别这个数字,所以每种类型所能表示的最大长度是有限的。为了让计算机能表示的数字尽可能多且占用位数尽可能少,聪明的人们发明了补码表示法,即第一位为符号位,并且参与计算。比如,一个 4 位的二进制补码数值1011,转换成十进制,就是−1×2^3+0×2^2+1×2^1+1×2^0=−5。如果最高位是 1,这个数必然是负数;最高位是 0,必然是正数。并且,只有 0000 表示 0,1000 在这样的情况下表示 -8。一个 4 位的二进制数,可以表示从 -8 到 7 这 16 个整数,不会白白浪费一位。正因为是这种实现,你会发现,如果是4位的二进制数,最大和最小之间仅仅差 1,7+1不会得到8,而是变成了-8,如此循环往复,溢出就是这个意思。
      溢出的问题其实是个大问题,但实际中却相对少发生,所以这里不过多展开,特别地,在高级开发语言里,往往都会有扩展大数计算的方法,比如Java里的BigInteger,就是专门用于大数科学计算领域。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值