商业计算工具——Bigdecimal详解
前言
对于浮点数的四则运算,我们知道可能会出现精度上的误差,对于不需要准确计算精度的计算我们可以使用
double
和float
来操作,但是涉及到金额交易(计算)等场景的时候,就需要我们特别注意结果的精度问题,这个时候,我们就可以采用Bigdecimal
类来进行高精度的四则运算了。本篇文章将对Bigdecimal的使用场景和具体的使用方法进行讲解,希望对读者能够有所帮助。
最前面的小例子
我们对下面的代码进行计算
@Test
public void mathTest(){
double b1 = 0.3;
double b2 = 4.2;
double b3 = 0.1;
double bb = 0.2;
double b4 = b2 - b1;
double b5 = b2 / b1;
double b6 = bb + b3;
double b7 = b3 * bb;
System.out.println("b4 = " + b4);
System.out.println("b5 = " + b5);
System.out.println("b6 = " + b6);
System.out.println("b7 = " + b7);
}
看上去答案很简单,但我们看一下打印结果:
我们发现,上述浮点数相加的结果并不是原先预想的值,原因在于我们的计算机是二进制的。
浮点数没有办法是用二进制进行精确表示。我们的CPU表示浮点数由两个部分组成:指数和尾数,这样的表示方法一般都会失去一定的精确度,有些浮点数运算也会产生一定的误差。如:2.4的二进制表示并非就是精确的2.4。反而最为接近的二进制表示是 2.3999999999999999。浮点数的值实际上是由一个特定的数学公式计算得到的。(后面有空会再出一篇文章来讲这个)
所以很多涉及商业计算的功能的话,最好使用Bigdecimal来统一进行计算。
Bigdecimal的使用详解
(一)常用构造函数
- public BigDecimal(double val)
*尽量不要使用
创建一个具有参数所指定双精度数值的对象 - public BigDecimal(String val)
创建一个具有参数所指定双精度数值的对象 - public BigDecimal(long val)
创建一个具有参数所指定双精度数值的对象 - public BigDecimal(int val)
创建一个具有参数所指定双精度数值的对象
使用构造函数的注意事项
我们看一下下面这个例子
@Test
public void DecimalTest(){
BigDecimal b1 = new BigDecimal(0.1);
BigDecimal b2 = new BigDecimal("0.1");
System.out.println("b1 = " + b1);
System.out.println("b2 = " + b2);
}
我们看到,虽然这里我们使用了入参为
double
类型的构造函数,但是当打印出来后我们看到结果依然不是我们预期的结果。所以我们使用Bigdecimal
的时候最好用String
类型的构造函数,如果原入参是double
的话,可以通过Bigdecimal
的静态方法valueOf
或者用String.valueOf
来进行定义。
@Test
public void DecimalTest(){
BigDecimal b1 = new BigDecimal(String.valueOf(0.1));
BigDecimal b2 = BigDecimal.valueOf(0.1);
System.out.println("b1 = " + b1);
System.out.println("b2 = " + b2);
}
(二)Bigdecimal的常见运算、
1. 四则运算
@Test
public void arithmeticTest(){
BigDecimal b1 = BigDecimal.valueOf(0.3);
BigDecimal b2 = BigDecimal.valueOf(4.2);
BigDecimal b3 = BigDecimal.valueOf(0.1);
BigDecimal b4 = BigDecimal.valueOf(0.2);
System.out.println("b2 - b1 = " + b2.subtract(b1));
System.out.println("b3 + b4 = " + b3.add(b4));
System.out.println("b3 * b4 = " + b3.multiply(b4));
System.out.println("b2 / b1 = " + b2.divide(b1));
System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1)));
}
我们可以看到,使用了Bigdecimal后,我们可以顺利地解决之前出现的精度问题了,但是在计算最后一个式子
10.1/3.1
的时候,却抛出了一个异常。这就引出了Bigdecimal的除法问题。
- Bigdecimal的除法
当使用该类进行除法运算时,如果出现无法整除的情况,那么程序就会抛出ava.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.的错误。这个时候应该怎么解决呢?
答案是使用devide的重载方法:
public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
第1个参数表示除数
第2个参数表示保留的小数位数
第3个参数表示四舍五入的模式
具体的模式如下:
ROUND_CEILING 向正无穷方向舍入
ROUND_DOWN 向零方向舍入
ROUND_FLOOR 向负无穷方向舍入
ROUND_HALF_DOWN 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向下舍入, 例如1.55 保留一位小数结果为1.5
ROUND_HALF_EVEN 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,如果保留位数是奇数,使用ROUND_HALF_UP,如果是偶数,使用ROUND_HALF_DOWN
ROUND_HALF_UP 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向上舍入, 1.55保留一位小数结果为1.6
ROUND_UNNECESSARY 计算结果是精确的,不需要舍入模式
ROUND_UP 向远离0的方向舍入
注意!!在java9之前,舍入模式是放在Bigdecimal的成员变量里面的,但是从Java9之后,推出了RoundingMode枚举类来替代原先的做法。如果使用还使用原先的做法的话,会提醒已过时,建议开发人员后续还是使用最新的规范来开发。
@Test
public void devideTest(){
System.out.println("10.1 / 3.1 = " + (10.1 / 3.1));
System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.HALF_UP)); // 四舍五入
System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.UP)); // 向上取整
System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.CEILING)); // 向上取整
System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.DOWN)); // 向下取整
System.out.println("10.1 / 3.1 = " + BigDecimal.valueOf(10.1).divide(BigDecimal.valueOf(3.1),2, RoundingMode.FLOOR));// 向下取整
}
2. Bigdecimal大小比较
对于Bigdecimal对象大小的比较比较简单,直接使用compareTo
@Test
public void compareTest(){
BigDecimal b1 = BigDecimal.valueOf(1.5);
BigDecimal b2 = BigDecimal.valueOf(2.5);
BigDecimal b3 = BigDecimal.valueOf(2.5);
System.out.println("b1 compare to b2 : " + b1.compareTo(b2));
System.out.println("b2 compare to b1 : " + b2.compareTo(b1));
System.out.println("b2 compare to b3 : " + b2.compareTo(b3));
}
打印结果如下:
我们可以看到,结果为-1表示小于,结果为1表示大于,结果为0表示等于
(三)Bigdecimal的格式化
我们可以使用NumberFormat
来对bigdecimal类型的数据进行格式化。下面举例百分比和货币格式的格式化方式。
@Test
public void formatTest(){
// 创建货币格式化对象
NumberFormat currencyInstance = NumberFormat.getCurrencyInstance();
// 创建百分比格式化对象
NumberFormat percentInstance = NumberFormat.getPercentInstance();
// 设置百分比最大的小数点位数为3位
percentInstance.setMaximumFractionDigits(3);
BigDecimal capital = BigDecimal.valueOf(10000.00);
BigDecimal rate = BigDecimal.valueOf(0.15123);
BigDecimal interest = capital.multiply(rate);
System.out.println("本金为: " + currencyInstance.format(capital));
System.out.println("利率为: " + percentInstance.format(rate));
System.out.println("利息为: " + currencyInstance.format(interest));
}
最后的小总结
上面我们先是讲了Bigdecimal
的使用背景,然后讲解了该类常用的方法,包括四则运算和数值间的比较,最后讲了对Bigdecimal
类型的格式化方法。
在使用过程中,还需要额外注意以下三点:
-
Bigdecimal
的性能相比double
和float
而言较差,所以如果对于精度要求不高的场景,可以不需要用该类来进行计算。 -
Bigdecimal
都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。 - 尽量避免直接用入参为double的构造函数创建Bigdecimal对象
参考文章:
Java之BigDecimal详解: https://www.cnblogs.com/zhangyinhua/p/11545305.html
Java BigDecimal详解: https://www.cnblogs.com/YangJavaer/p/6056394.html