背景
从事金融相关项目,对BigDecimal
应该是再熟悉不过了,也有很多人因为不知道、不了解或使用不当导致资损事件发生。
所以,如果你从事金融相关项目,或者你的项目中涉及到金额的计算,那么你一定要花时间看看这篇文章,全面学习一下BigDecimal
概述
一般情况下,对于不需要准确计算精度的数字,可以直接使用Float
和Double
处理,但是 Float
和Double
会导致精度丢失。所以在需要精确计算结果的项目中,则必须使用BigDecimal
类来操作。虽然,BigDecimal
比 Float
和Double
能够保证精度问题,但是使用不当也会踩坑。
四个BigDecimal 中容易踩的坑
1、创建 BigDecimal 的坑
在BigDecimal
中提供了多种创建方式,可以通过new
直接创建,也可以通过 BigDecimal#valueOf
创建。这两种方式使用不当,也会导致精度问题。如下:
@Test
public void test(){
BigDecimal b1 = new BigDecimal(0.1);
BigDecimal b2 = BigDecimal.valueOf(0.1);
System.out.println("b1=" + b1);
System.out.println("b2=" + b2);
}
输出结果:
b1=0.1000000000000000055511151231257827021181583404541015625
b2=0.1
上面示例中 b1
还是出现了精度的问题。造成这种问题的原因是 0.1 这个数字计算机是无法精确表示的,送给 BigDecimal 的时候就已经丢精度了,而 BigDecimal#valueOf
的实现却完全不同。如下源码所示,BigDecimal#valueOf
中是把浮点数转换成了字符串来构造的BigDecimal
,因此避免了问题。
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}
结论
:
第一,在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型;
第二,如果无法满足第一条,则可采用BigDecimal#valueOf方法来构造初始化值。
2、等值比较的坑
一般在比较两个值是否相等时,都是用equals
方法,但是,在BigDecimal
中使用equals
可能会导致结果错误,BigDecimal
中提供了 compareTo
方法,在很多时候需要使用compareTo
比较两个值。如下所示:
@Test
public void testEquals(){
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
出现此种结果的原因是,equals
不仅比较了值相等,还比较了精度是否相同。示例中,由于两个值的精度不同,所有结果也就不相同。而 compareTo
是值比较值的大小。返回的值为-1(小于),0(等于),1(大于)。
结论
如果比较两个BigDecimal
值的大小,采用其实现的compareTo
方法;
如果严格限制精度的比较,那么则可考虑使用equals
方法。
3、无限精度的坑
BigDecimal
并不代表无限精度,当在两个数除不尽的时候,就会出现无限精度的坑,如下所示:
@Test
public void testDivide(){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("3.0");
b1.divide(b2);
}
输出结果:
java.lang.ArithmeticException: Non-terminating decimal expansion;
no exact representable decimal result.
在官方文档中有如下说明:
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
方法时指定结果的精度即可:
@Test
public void testDivide(){
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("3.0");
BigDecimal divide = b1.divide(b2, 2, RoundingMode.HALF_UP);
System.out.println(divide);
}
结论
在使用BigDecimal
进行(所有)运算时,尽量指定精度和舍入模式。
4、输出字符串的坑
在BigDecimal
转换成字符串时,有可能输出非你预期的结果。如下所示:
@Test
public void testString(){
BigDecimal a = BigDecimal.valueOf(332334535345456700.12345634534534578901);
System.out.println(a.toString());
}
输出结果:
3.323345353454567E+17
可以看到结果已经被转换成了科学计数法,可能这个并不是预期的结果 ,BigDecimal
有三个方法可以转为相应的字符串类型,切记不要用错:
String toString(); // 有必要时使用科学计数法
String toPlainString(); // 不使用科学计数法
String toEngineeringString(); // 工程计算中经常使用的记录数字的方法,与科学计数法类似,但要求10的幂必须是3的倍数
小结
在金融项目中,一般都是推荐使用BigDecimal
,来避免精度的丢失。但是BigDecimal
使用不当也会踩坑。本章内容主要介绍了在使用BigDecimal
时经常容易踩的坑。虽然某些场景下推荐使用BigDecimal
,它能够达到更好的精度,但性能相较于double
和float
,还是有一定的损失的,特别在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal
。而必须使用时,一定要规避上述的坑。