踩坑一:创建时精度丢失
在BigDecimal 中提供了多种创建方式,可以通过new 直接创建,也可以通过 valueOf 创建。这两种方式使用不当,也会导致精度问题。如下:
public static void main(String[] args) throws Exception {
BigDecimal b1= new BigDecimal(0.1);
System.out.println(b1);
BigDecimal b2= BigDecimal.valueOf(0.1);
System.out.println(b2);
BigDecimal b3= BigDecimal.valueOf(0.111111111111111111111111111234);
System.out.println(b3);
}
执行结果:
0.1000000000000000055511151231257827021181583404541015625
0.1
0.1111111111111111
上面示例中两个方法都传入了double 类型的参数 0.1,但是 b1 还是出现了精度的问题。
造成这种问题的原因是 0.1 这个数字计算机是无法精确表示的,
给 BigDecimal 的时候就已经丢精度了,而BigDecimal#valueOf 的实现却完全不同。
如下源码所示,BigDecimal# valueOf 中是 先把把浮点数转换成了字符串 来构造的BigDecimal,因此避免了问题。
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
结论:
1> 使用 BigDecimal 构造函数时,尽量传递 字符串 而非 浮点类型;
2> 如果无法满足第一条,则可采用 BigDecimal#valueOf
方法来构造初始化值。但 valueOf 受 double 类型精度影响,当传入参数小数点后的位数 超过 double 允许的16位精度,还是可能会出现问题的。
踩坑二:等值比较的坑
一般在比较两个值是否相等时,都是用equals 方法,但是,在BigDecimal 中使用equals可能会导致结果错误,BigDecimal 中提供了 compareTo 方法,在很多时候需要使用compareTo 比较两个值。如下所示:
public static void main(String[] args){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("1.00");
System.out.println(b1.equals(b2));
System.out.println(b1.compareTo(b2));
}
执行结果:
false
0
原因是
1> equals不仅比较了值是否相等,还比较了精度是否相同。
示例中,由于两个值的精度不同,所有结果也就不相同。
2> compareTo 是只比较值的大小。返回的值为 -1(小于),0(等于),1(大于)。
结论:
如果比较两个 BigDecimal 值大小,采用 compareTo
方法;
如果 严格限制精度的比较,那么则可考虑使用 equals
方法。
踩坑三:无限精度
BigDecimal 并不代表无限精度,当在两个数除不尽的时候,就会出现无限精度的坑,如下所示:
public static void main(String[] args){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("3.0");
b1.divide(b2);
}
执行结果:
在官方文档中对该异常有如下说明:
If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.
大致意思就是,如果在除法(divide)运算过程中,如果商是一个无限小数(如 0.333…),而操作的结果预期是一个精确的数字,那么将会抛出 ArithmeticException 异常。
此种情况,只需要在使用 divide方法时指定结果的精度即可:
public static void main(String[] args){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("3.0");
System.out.println(b1.divide(b2,2, RoundingMode.HALF_UP)); //0.33
}
结论:
在使用BigDecimal进行(所有)运算时,尽量指定精度 和 舍入模式。
踩坑四:BigDecimal 三种字符串输出
在 BigDecimal 转换成字符串时,有可能输出非你预期的结果。
如下所示:
public static void main(String[] args){
BigDecimal bg = new BigDecimal("1E11");
System.out.println(bg.toString()); // 1E+11
System.out.println(bg.toPlainString()); // 100000000000
System.out.println(bg.toEngineeringString()); // 100E+9
}
执行结果:
1E+11
100000000000
100E+9
下面介绍 java.math.BigDecimal 下的三个toString方法的区别及用法
- toString() :有必要时使用科学计数法。
- toPlainString() : 不使用任何指数。
- toEngineeringString():有必要时使用工程计数法。
工程记数法 是一种 工程计算 中经常使用的记录数字的方法,与科学技术法类似,但要求 10的幂必须是3的倍数。
踩坑五:使用BigDecimal进行计算时参数不能为NULL
在使用BigDecimal 进行加、减、乘、除、比较大小时,
要保证参与计算的两个值不能为空,否则会有 NPE 。
代码示例:
BigDecimal b1 = new BigDecimal("1");
BigDecimal b2 = null;
System.out.println("相加:" + b2.add(b1));
结果:
踩坑六:除法计算时 除数不能为0
代码示例:
BigDecimal b1 = new BigDecimal("1");
BigDecimal b2 = new BigDecimal("0");
System.out.println(b1.divide(b2));
执行结果:
踩坑七:执行顺序不能调换(乘法交换律失效)
乘法满足交换律是一个常识,但是在计算机的世界里,会出现不满足乘法交换律的情况;
代码示例:
BigDecimal b1 = BigDecimal.valueOf(1.0);
BigDecimal b2 = BigDecimal.valueOf(3.0);
BigDecimal b3 = BigDecimal.valueOf(3.0);
// b1 / b2 * b3
System.out.println(b1.divide(b2, 2, RoundingMode.HALF_UP).multiply(b3)); // 0.990
// b1 * b3 / b2
System.out.println(b1.multiply(b3).divide(b2, 2, RoundingMode.HALF_UP)); // 1.00
执行结果:
0.990
1.00
执行顺序交换后,产生的结果可能不同,会导致一定的问题,使用顺序建议先乘后除。