摘要 详细阐述在使用 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)举个实际案例。在 用大模型计算房贷还款,有误差。也不知您多还款了吗? 或 用大模型计算房贷还款,有误差。也许,您还贷少了! 的等额本息还款金额计算中:
其中:
- P 是贷款本金
- i 是月利率(年利率除以12)
- n 是还款期数(年数乘以12)
不妨设年利率为5%,这时若月利率 i 取 5位小数:
则极大可能影响后面的结果。所以,不论是利用 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