为什么要用BigDecimal?
因为float和double是不精确的,在进行一些金额相关的计算时,对准确度要求较高,因此要使用BigDecimal。
为什么float和double是不精确的呢?
这要从十进制转二进制讲起,整数转为二进制,采用的是"除2取余,倒序排列"。比如10转为二进制是1010,计算如下:
10 / 2 = 5 余 0
5 / 2 = 2 余 1
2 / 2 = 1 余 0
1 / 2 = 0 余 1
当整数部分变为0时停止计算,此时取余数的倒叙排列,则为1010。
而小数转为二进制,采用的是"乘2取整,正序排列"。比如0.625变为小数是0.101,计算如下:
0.625 * 2 = 1.25 --取出整数部分 1
0.25 * 2 = 0.5 --取出整数部分 0
0.5 * 2 = 1 --取出整数部分 1
没有小数时停止计算,此时,取整数的正序排列,则为 0.101。
那么所有小数都能精确的转为二进制吗?
并不是,比如0.1要转为二进制,我们计算一下:
0.1 * 2 = 0.2 --取出整数部分 0
0.2 * 2 = 0.4 --取出整数部分 0
0.4 * 2 = 0.8 --取出整数部分 0
0.8 * 2 = 1.6 --取出整数部分 1
0.6 * 2 = 1.2 --取出整数部分 1
0.2 * 2 = 0.4 --取出整数部分 0
······
到这里就发现了,0.1 转为二进制是0.000110......,会一直循环下去,所以说float和double是不精确的。
为什么BigDecimal是精确的?
BigDecimal是由无标度值和标度两部分组成,比如 3.12 在 BigDecimal内部其实形式为 (312,2),312是无标度值,而2是标度,也就是小数位数。很明显 312 是整数,可以精确的转为二进制,因此BigDecimal是精确的。
BigDecimal要避免的一些坑
那么既然BigDecimal是精确的,是不是就可以随意使用呢?实际上BigDecimal也有一些需要注意的地方,否则就会掉坑里了。
BigDecimal的创建
有以下几种创建方式
arduino
public BigDecimal(double val);
public BigDecimal(long val);
public BigDecimal(String val);
BigDecimal.valueOf(double val);
BigDecimal.valueOf(long val);
那么采用这几种方式创建一个0.1,看下结果会有什么不同?
csharp
public static void testDecimal() {
BigDecimal a = new BigDecimal(0.1);
BigDecimal b = new BigDecimal("0.1");
BigDecimal c = BigDecimal.valueOf(0.1);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
}
结果为:
a = 0.1000000000000000055511151231257827021181583404541015625
b = 0.1
c = 0.1
有人就疑惑了,上面不是刚说到BigDecimal是精确的吗,怎么这里通过new BigDecimal(double val)方式创建的就不是精确的了。这是因为传进去的0.1是个double值,而double转为二进制后是不精确的,实际上传的值是new BigDecimal(0.10000000000000000555111...) ,因此BigDecimal也就不再是0.1了。
为什么BigDecimal.valueOf(0.1)方式传进的也是double值,是精确的呢,我们看下源码
Double.toString(0.10000000000000000555111...),经过计算,最后会变为字符串0.1,因此BigDecimal也就为0.1了。
因此对于double转BigDecimal,要采用 new BigDecimal(String val) 方式或者BigDecimal.valueOf() 方式,不能使用new BigDecimal(double val)方式。
BigDecimal加减乘除运算
加减乘除计算
csharp
public static void testDecimal2() {
BigDecimal a = new BigDecimal("2.5");
BigDecimal b = new BigDecimal("2.0");
BigDecimal c = new BigDecimal("1.5");
System.out.println("a + b = " + a.add(b));
System.out.println("a - b = " + a.subtract(b));
System.out.println("a * b = " + a.multiply(b));
System.out.println("a / b = " + a.divide(b));
}
计算结果如下
如果计算 a / c 会怎样呢,我们试下
csharp
System.out.println("a / c = " + a.divide(c));
结果是
这个错误告诉我们 a / c 除不尽,所以报错了。正确写法应该如下所示:
csharp
System.out.println("a / c = " + a.divide(c,2, RoundingMode.HALF_UP));
2代表保留2位小数,RoundingMode.HALF_UP代表是四舍五入,和BigDecimal.ROUND_HALF_UP是一样的。RoundingMode除了HALF_UP模式,还有很多类型,比如UP模式,进入RoundingMode.UP看看注释,就能清楚的理解其含义
多项计算用法:
css
System.out.println("a / c * b = " + a.divide(c,10, RoundingMode.HALF_UP).multiply(b).setScale(2,RoundingMode.HALF_UP));
a / c 时保留10位小数,是为了中间的值更加精确,然后乘以 b ,最后再通过setScale(2,RoundingMode.HALF_UP) 四舍五入保留2位小数。
BigDecimal比较大小
看下equals和compareTo两个方法有什么区别
csharp
public static void testDecimal3() {
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println("a.equals(b) = " + a.equals(b));
//compareTo 0为相等,1为大于,-1是小于
boolean value = a.compareTo(b) == 0 ? true : false;
System.out.println("a.compareTo(b) = " + value);
}
结果如下:
如果前面的内容认真看了,这里应该能大致猜到。BigDecimal("1.0") 实际内部的形式是 (10,1),而BigDecimal("1.00")实际内部形式是(100,2),它们两个的无标度值和标度都不同,所以通过equals比较是不等的。比较大小应该用compareTo方法去比较。
总结
1.在涉及金额计算等精确度要求较高的场景,应该使用BigDecimal。
2.BigDecimal创建对象时应该使用字符串构造器方法或者BigDecimal.valueOf()方法。
3.BigDecimal在复杂计算时,除法计算要带上精度,最后的值也要设置精度。
4.BigDecimal比较大小时应采用compareTo()方法比较