详述 BigDecimal 的错误计算

摘要  详细阐述在使用 Java 的 BigDecimal 类时,可能产生的错误计算。    

       据 java中BigDecimal的介绍及使用,BigDecimal格式化,BigDecimal常见问题-CSDN博客 介绍:“BigDecimal 的执行顺序不能调换(乘法交换律失效)。” 

       看下面代码:

BigDecimal b1 = BigDecimal.valueOf(1.0);
BigDecimal b2 = BigDecimal.valueOf(3.0);
BigDecimal b3 = BigDecimal.valueOf(3.0);
System.out.println(b1.divide(b2, 2, RoundingMode.HALF_UP).multiply(b3)); // 0.990
System.out.println(b1.multiply(b3).divide(b2, 2, RoundingMode.HALF_UP)); // 1.00

上面代码分别输出 (b1/b2*b3) 与 (b1*b3/b2)的结果, 其中 b1=1.0, b2=3.0, b3=3.0, 即输出(1.0/3.0*3.0)与(1.0*3.0/3.0)的值。

       运行结果为:

这样,BigDecimal 类的连续运算产生了误差。

       其实,BigDecimal 存在上述问题,一点也不奇怪。

例1.  计算 45.68/2.88*123456 . 

       不妨编程如下:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class example0{
    public static void main(String[] args) {
        
          BigDecimal b1 = new BigDecimal("45.68");
          BigDecimal b2 = new BigDecimal("2.88");
          BigDecimal b3 = new BigDecimal("123456"); 
          
          // 除法不妨保留2位小数(比如模仿金额的精确到分)
          System.out.println(b1.divide(b2, 2, RoundingMode.HALF_UP).multiply(b3));
    }
}

运行后,有输出:

然而,正确值是 1958149.33(保留2位小数)。这样,输出结果中对应红色数字的数字是错误数字。利用 BigDecimal 计算时,输出的有效数字的正确率只有 4/9 = 44.44% .

  例2.  不妨再计算 45/88888*67与 45*67/88888,以便比较它们的结果。它们的最长位数是5,那么不妨取 divide 的第2个参数也为5 . 

       代码如下:

BigDecimal b1 = BigDecimal.valueOf(45);
BigDecimal b2 = BigDecimal.valueOf(88888);
BigDecimal b3 = BigDecimal.valueOf(67);
System.out.println(b1.divide(b2, 5, RoundingMode.HALF_UP).multiply(b3)); // 0.03417 
System.out.println(b1.multiply(b3).divide(b2, 5, RoundingMode.HALF_UP)); // 0.03392

这时,运行后的输出为:

这样,两个结果只有1位相同有效数字。于是,不需要知道正确值,也能判断有一个输出是错误的。 

例3.  计算 (1*3/3-1)*5e10 与 (1/3*3-1)*5e10 .

       主要代码如下:

BigDecimal a = BigDecimal.valueOf(1);
BigDecimal b = BigDecimal.valueOf(3);
BigDecimal c = BigDecimal.valueOf(3);
BigDecimal d = BigDecimal.valueOf(5e10);

// 计算 ((a * b / c) - 1) * d
BigDecimal result1 = a.multiply(b).divide(c, 5, RoundingMode.HALF_UP).subtract(BigDecimal.ONE).multiply(d);
        
// 计算 ((a / c * b) - 1) * d
BigDecimal result2 = a.divide(c, 5, RoundingMode.HALF_UP).multiply(b).subtract(BigDecimal.ONE).multiply(d);

       运行后,result1 与 result2 的值为:  

这样,正确结果是,但是,将"*3"与"/3"调换计算顺序后,输出成了负五十万 .

点评:

    (1)BigDecimal 只能保证单个运算的准确性。正如 MPFR 软件的作者对自己的软件所做的评价: “只能保证一个原子运算的正确舍入(it only guarantees correct rounding for an atomic operation)”[1]。

    (2)对于复合运算,则不能保证不出错。因为,舍入误差的定量分析是一个困难的问题[2]。

    (3)本质原因是 BigDecimal 有许多参数要设置。比如,divide中要设置保留的小数位数,与舍入方式,或还有Java中的 MathContext参数的设置。如果这些参数设置错误,则可能得出错误结果。然而,并没有文档告诉用户,究竟如何设置这些参数。所以,用户只能凭经验去设置。这样,一不小心,就会出错。

    (4)关于参数的设置问题,其实也是现有编程模式存在的问题,即软件系统普遍存在的问题。比如,Maple有 Digits、Matlab有 digits、Mathematica有 $MaxExtraPrecision、Pari有 \p 以及 Gmp有 mpf_set_default_prec、MPFR有 mpfr_set_default_prec等等。这些参数的设置交由用户来负责。

    (5)举个实际案例。在 用大模型计算房贷还款,有误差。也不知您多还款了吗? 或 用大模型计算房贷还款,有误差。也许,您还贷少了! 的等额本息还款金额计算中:

\displaystyle M=\frac{P\times i\times(1+i)^n}{(1+i)^n-1}

其中:

  • P 是贷款本金
  • i 是月利率(年利率除以12)
  • n 是还款期数(年数乘以12)

不妨设年利率为5%,这时若月利率 i 取 5位小数:

i=\frac{5\%}{12}=\frac{0.05}{12}\approx 0.00417\,,

则极大可能影响后面的结果。所以,不论是利用 BigDecimal的 divide方法计算除法,还是使用别的软件计算,均会遇到保留多少位的问题。

    (6)ISRealsoft 没有上述问题。

参考文献

[1] Zimmermann P. Reliable Computing with GNU MPFR. In: Fukuda K et al. eds. ICMS 2010, LNCS 6327. Berlin: Springer, 2010. 42--45

[2] 李庆扬, 王能超, 易大义. 数值分析. 第5版. 北京: 清华大学出版社, 2008. 18

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值