likes
comments
collection
share

BigDecimal需要避免的一些坑

作者站长头像
站长
· 阅读数 9

为什么要用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的创建

有以下几种创建方式

public BigDecimal(double val);
public BigDecimal(long val);
public BigDecimal(String val);

BigDecimal.valueOf(double val);
BigDecimal.valueOf(long val);

那么采用这几种方式创建一个0.1,看下结果会有什么不同?

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值,是精确的呢,我们看下源码

BigDecimal需要避免的一些坑

Double.toString(0.10000000000000000555111...),经过计算,最后会变为字符串0.1,因此BigDecimal也就为0.1了。

因此对于double转BigDecimal,要采用 new BigDecimal(String val) 方式或者BigDecimal.valueOf() 方式,不能使用new BigDecimal(double val)方式。

BigDecimal加减乘除运算

加减乘除计算

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));
}

计算结果如下

BigDecimal需要避免的一些坑

如果计算 a / c 会怎样呢,我们试下

System.out.println("a / c = " + a.divide(c));

结果是

BigDecimal需要避免的一些坑

这个错误告诉我们 a / c 除不尽,所以报错了。正确写法应该如下所示:

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看看注释,就能清楚的理解其含义

BigDecimal需要避免的一些坑

多项计算用法:

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两个方法有什么区别

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需要避免的一些坑

如果前面的内容认真看了,这里应该能大致猜到。BigDecimal("1.0") 实际内部的形式是 (10,1),而BigDecimal("1.00")实际内部形式是(100,2),它们两个的无标度值和标度都不同,所以通过equals比较是不等的。比较大小应该用compareTo方法去比较。

总结

1.在涉及金额计算等精确度要求较高的场景,应该使用BigDecimal。

2.BigDecimal创建对象时应该使用字符串构造器方法或者BigDecimal.valueOf()方法。

3.BigDecimal在复杂计算时,除法计算要带上精度,最后的值也要设置精度。

4.BigDecimal比较大小时应采用compareTo()方法比较