目录
在互联网金融业务领域对于金额的使用、计算等,Java常用BigDecimal类型对数据精度、精确计算等要求场景内。但在使用该java.math.BigDecimal类时,注意不要踩坑。用错可能会有大问题,大灾难!金额无小事!资损无小事!本文用于介绍BigDecimal的常用方法及整理的小小的扫盲避坑指南。
Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以开发中,如果我们需要精确计算的结果,则必须使用BigDecimal类来操作。
BigDecimal(int)
创建一个具有参数所指定整数值的对象
BigDecimal(double)
创建一个具有参数所指定双精度值的对象
BigDecimal(long)
创建一个具有参数所指定长整数值的对象
BigDecimal(String)
创建一个具有参数所指定以字符串表示的数值的对象
BigDecimal的使用的第一步就是创建一个BigDecimal对象,如果这一步都有问题,那么后面怎么算都是错的!
那到底应该如何正确的创建一个BigDecimal?
关于这个问题,在《阿里巴巴Java开发手册》中有一条建议,或者说是要求:
这是一条【强制】建议,那么,这背后的原理是什么呢?想要搞清楚这个问题,主要需要弄清楚以下几个问题:
1、为什么说double不精确?
2、BigDecimal是如何保证精确的?
在知道这两个问题的答案之后,我们也就大概知道为什么不能使用BigDecimal(double)来创建一个BigDecimal了。double为什么不精确?首先,计算机是只认识二进制的,即0和1,这个大家一定都知道。那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。十进制整数转成二进制很简单,通常采用"除2取余,逆序排列"即可,如10的二进制为1010。
但是,小数的二进制如何表示呢?
十进制小数转成二进制,一般采用"乘2取整,顺序排列"方法,如0.625转成二进制的表示为0.101。但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数。
所以,计算机是没办法用二进制精确的表示0.1的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来。
在Java中,使用float和double分别用来表示单精度浮点数和双精度浮点数。所谓精度不同,可以简单的理解为保留有效位数不同。采用保留有效位数的方式近似的表示小数。
所以,大家也就知道为什么double表示的小数不精确了。
BigDecimal如何精确计数?
如果大家看过BigDecimal的源码,其实可以发现,实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。在BigDecimal中,标度是通过scale字段来表示的。
而无标度值的表示比较复杂。当unscaled value超过阈值(默认为Long.MAX_VALUE)时采用intVal字段存储unscaled value,intCompact字段存储Long.MIN_VALUE,否则对unscaled value进行压缩存储到long型的intCompact字段用于后续计算,intVal为空。
BigDecimal所创建的是对象,故我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。
BigDecimal舍入模式
尽管数据库存储的是一个高精度的浮点数,但是通常在应用中展示的时候往往需要限制一下小数点的位数,比如两到三位小数即可,这时就需要使用到setScale(int newScale, int roundingMode)函数,作为BigDecimal的公有静态变量,舍入模式(Rounding Mode)的运算规则比较多,共有八种,这里作个说明,官方文档也有介绍。
java中对BigDecimal比较大小一般用的是BigDecimal的compareTo方法。
举例如:
int a = bigdemical.compareTo(bigdemical2)
返回结果分析:
a = -1,表示bigdemical小于bigdemical2;
a = 0,表示bigdemical等于bigdemical2;
a = 1,表示bigdemical大于bigdemical2;
- 举例:a大于等于b
-
- new bigdemica(a).compareTo(new bigdemical(b)) >= 0
1.初始化创建对象使用new BigDecimal()
还是BigDecimal#valueOf()
?
先看下面这段代码
- BigDecimal bd1 = new BigDecimal(0.01);
- BigDecimal bd2 = BigDecimal.valueOf(0.01);
- System.out.println("bd1 = " + bd1);
- System.out.println("bd2 = " + bd2);
输出到控制台的结果是:
- bd1 = 0.01000000000000000020816681711721685132943093776702880859375
- bd2 = 0.01
造成这种差异的原因是0.1这个数字计算机是无法精确表示的,送给BigDecimal
的时候就已经丢精度了,而BigDecimal#valueOf
的实现却完全不同
- 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
对象,如果实在不行,就使用BigDecimal#valueOf()
方法吧。
2. 等值比较方法 使用equals还是compareTo ?
- BigDecimal bd1 = new BigDecimal("1.0");
- BigDecimal bd2 = new BigDecimal("1.00");
- System.out.println(bd1.equals(bd2));
- System.out.println(bd1.compareTo(bd2));
控制台的输出将会是:
- false
- 0
结论:使用compareTo方法。
究其原因是,BigDecimal
中equals
方法的实现会比较两个数字的精度,而compareTo
方法则只会比较数值的大小。
3.BigDecimal
并不代表无限精度 除法保留精度问题,巧用舍入模式
先看这段代码
- BigDecimal a = new BigDecimal("1.0");
- BigDecimal b = new BigDecimal("3.0");
- a.divide(b) // results in the following exception.
结果会抛出异常:
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
原因:关于这个异常,Oracle的官方文档有具体说明
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.
大意是,如果除法的商的结果是一个无限小数但是我们期望返回精确的结果,那程序就会抛出异常。回到我们的这个例子,我们需要告诉JVM
我们不需要返回精确的结果就好了。
正确使用方式:
- BigDecimal a = new BigDecimal("1.0");
- BigDecimal b = new BigDecimal("3.0");
- a.divide(b, 2, RoundingMode.HALF_UP)// 0.33
注:RoundingMode类型可参看上述舍入模式说明,按需使用
4.BigDecimal
转回String类型
要小心避坑
- BigDecimal d = BigDecimal.valueOf(12334535345456700.12345634534534578901);
- String out = d.toString(); // Or perform any formatting that needs to be done
- System.out.println(out); // 1.23345353454567E+16
可以看到结果已经被转换成了科学计数法,可能这个并不是预期的结果BigDecimal
有三个方法可以转为相应的字符串类型,切记不要用错:
正确使用方式:
- String toString(); // 有必要时使用科学计数法
- String toPlainString(); // 不使用科学计数法
- String toEngineeringString(); // 工程计算中经常使用的记录数字的方法,与科学计数法类似,但要求10的幂必须是3的倍数
5.执行顺序不能调换(乘法交换律失效)
乘法满足交换律是一个常识,但是在计算机的世界里,会出现不满足乘法交换律的情况!!
- BigDecimal a = BigDecimal.valueOf(1.0);
- BigDecimal b = BigDecimal.valueOf(3.0);
- BigDecimal c = BigDecimal.valueOf(3.0);
- System.out.println(a.divide(b, 2, RoundingMode.HALF_UP).multiply(c)); // 0.990
- System.out.println(a.multiply(c).divide(b, 2, RoundingMode.HALF_UP)); // 1.00
别小看这这0.01的差别,在汇金领域,会产生非常大的金额差异!!