BigDecimal 类
# BigDecimal 类
如果要进行高精度浮点值的计算,可以用 java.math 包中的 BigDecimal 类。
什么时候我们需要用到高精度的浮点数运算呢?当处理金额的时候,绝对不能有精度损失;而之前我们说过,double 和 float 是不精确的(参考我写的 Java 中的浮点数 (opens new window))
为了解决浮点数的精度问题,一些编程语言引入了十进制的小数 Decimal
。Decimal
在不同社区中都十分常见,如果编程语言没有原生支持 Decimal
,我们在开源社区也一定能够找到使用特定语言实现的 Decimal
库。例如 Java 通过 BigDecimal
提供了无限精度的小数。
BigDecimal 类可以用于表示任意精度的数。特点如下:
- 可以用
new BigDecimal(String)
来创建对象,或者new BigDecimal(double)
,构造方法有很多。 - 可以用
add
、substr
、multiple
、divide
和remainder(取余)
方法来完成算数运算 - 是不可变的。任何针对 BigDecimal 对象的修改都会产生一个新对象(类似 String)
- 如果除法运算过程不能终止(例如除不尽),会抛出 ArithmeticException 异常;但可以使用重载的方法来指定尺度(保留几位小数)和舍入方式(向上取整、向下取整还是四舍五入)来避免异常:
divide(BigDecimal d, int scale, int roundingMode)
# 常用方法
BigDecimal 有很多的构造方法,例如:
- BigDecimal(int) 创建一个具有参数所指定整数值的对象
- BigDecimal(double) 创建一个具有参数所指定双精度值的对象
- BigDecimal(long) 创建一个具有参数所指定长整数值的对象
- BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象
常用的方法如下:
- add(BigDecimal) BigDecimal 对象中的值相加,然后返回这个对象。
- subtract(BigDecimal) BigDecimal 对象中的值相减,然后返回这个对象。
- multiply(BigDecimal) BigDecimal 对象中的值相乘,然后返回这个对象。
- divide(BigDecimal) BigDecimal 对象中的值相除,然后返回这个对象。
- toString() 将 BigDecimal 对象的数值转换成字符串。
- doubleValue() 将 BigDecimal 对象中的值以双精度数返回。
- floatValue() 将 BigDecimal 对象中的值以单精度数返回。
- longValue() 将 BigDecimal 对象中的值以长整数返回。
- intValue() 将 BigDecimal 对象中的值以整数返回。
# 演示 ArithmeticException 异常
import java.math.BigDecimal;
public class BigDecimalDemo1 {
public static void main(String[] args) {
BigDecimal a = new BigDecimal(1.0);
BigDecimal b = new BigDecimal(3);
a.divide(b);
}
}
2
3
4
5
6
7
8
9
> javac BigDecimalDemo1.java
> java BigDecimalDemo1
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1693)
at BigDecimalDemo1.main(BigDecimalDemo1.java:7)
2
3
4
5
# 演示重载的 divide
import java.math.BigDecimal;
public class BigDecimalDemo2 {
public static void main(String[] args) {
BigDecimal a = new BigDecimal(1.0);
BigDecimal b = new BigDecimal(3);
System.out.println( a.divide(b, 20, BigDecimal.ROUND_UP));
BigDecimal d = new BigDecimal("-1");
BigDecimal e = new BigDecimal("3");
System.out.println(d.divide(e, 20, BigDecimal.ROUND_UP));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
编译和运行:
> javac BigDecimalDemo2.java
> java BigDecimalDemo2
0.33333333333333333334
-0.33333333333333333334
2
3
4
这里舍入方式用了 BigDecimal
的 ROUND_UP,表明向上取整。BigDecimal
还有以下取整方式(都是 BigDecimal 的成员 static 变量,本质都是 int 值,只不过 BigDecimal
定义了个别名,方便)
ROUND_UP 远离零的舍入模式。 始终对非零舍弃部分前面的数字加 1。例如:2.36 -> 2.4 (2.4 比 2.36 更加远离 0)。可以理解为删除多余的小数位,并在最后一个数字上 +1.
ROUND_DOWN 接近零的舍入模式。例如:2.36 -> 2.3 (2.3 比 2.36 更靠近 0)。
ROUND_CEILING 接近正无穷大的舍入模式。
如果 BigDecimal 为正,则舍入行为与 ROUND_UP 相同;
如果为负,则舍入行为与 ROUND_DOWN 相同。
相当于是 ROUND_UP 和 ROUND_DOWN 的合集
ROUND_FLOOR 接近负无穷大的舍入模式。与 ROUND_CEILING 正好相反
ROUND_HALF_UP 就是四舍五入,例如:2.35 -> 2.4
ROUND_HALF_DOWN 就是五舍六入,例如:2.35 -> 2.3
ROUND_HALF_EVEN 如果舍弃部分左边的数字为奇数,则舍入行为与 ROUND_HALF_UP 相同(四舍五入);如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同(五舍六入)。例如:1.15 -> 1.1,1.25 -> 1.2
在重复进行一系列计算时,此舍入模式可以在统计上将累加错误减到最小。此舍入模式也称为“银行家舍 入法”,主要在美国使用。
ROUND_UNNECESSARY 这个模式名副其实,确实是"unnecessary",其实就和没设置精度一样, 如果相除的结果不是精确值 则抛出一个 ArithmeticException 异常.
更多详见官方文档 BigDecimal (Java Platform SE 8 ) (opens new window)
小结:常用的就是 ROUND_HALF_UP、ROUND_UP 和 ROUND_DOWN,其它的看看就行
import java.math.BigDecimal;
public class BigDecimalDemo2 {
public static void main(String[] args) {
BigDecimal a = new BigDecimal(1.0);
BigDecimal b = new BigDecimal(3);
System.out.println("ROUND_UP: " + a.divide(b, 20, BigDecimal.ROUND_UP)); //0.33333333333333333334
System.out.println("ROUND_DOWN: " + a.divide(b, 20, BigDecimal.ROUND_DOWN)); //0.33333333333333333333
System.out.println("ROUND_CEILING: " + a.divide(b, 20, BigDecimal.ROUND_CEILING)); //0.33333333333333333334
System.out.println("ROUND_FLOOR: " + a.divide(b, 20, BigDecimal.ROUND_FLOOR)); //0.33333333333333333333
System.out.println("ROUND_HALF_UP: " + a.divide(b, 20, BigDecimal.ROUND_HALF_UP)); //0.33333333333333333333
BigDecimal d = new BigDecimal("-1");
BigDecimal e = new BigDecimal("3");
System.out.println("ROUND_UP: " + d.divide(e, 20, BigDecimal.ROUND_UP)); //-0.33333333333333333334
System.out.println("ROUND_DOWN: " + d.divide(e, 20, BigDecimal.ROUND_DOWN)); //-0.33333333333333333333
System.out.println("ROUND_CEILING: " + d.divide(e, 20, BigDecimal.ROUND_CEILING)); //-0.33333333333333333333
System.out.println("ROUND_FLOOR: " + d.divide(e, 20, BigDecimal.ROUND_FLOOR)); //-0.33333333333333333334
System.out.println("ROUND_HALF_UP: " + d.divide(e, 20, BigDecimal.ROUND_HALF_UP)); //-0.33333333333333333334
BigDecimal f = new BigDecimal("0.7");
BigDecimal g = new BigDecimal("2");
System.out.println("ROUND_HALF_UP: " + f.divide(g, 1, BigDecimal.ROUND_HALF_UP)); //0.4
System.out.println("ROUND_HALF_DOWN: " + f.divide(g, 1, BigDecimal.ROUND_HALF_DOWN)); //0.3
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# BigDecimal 的缺点
像 BigInteger
和 BigDecimal
这种大数类的运算效率肯定是不如原生类型效率高,代价还是比较昂贵的,是否选用需要根据实际场景来评估。我们来尝试比较一下:
import java.math.BigDecimal;
public class BigDecimalDemo4 {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
double d = 1.0;
for(int i = 0; i < 100000; i++){
d++;
}
long endTime = System.currentTimeMillis();
System.out.println("double程序运行时间:" + (endTime - startTime) + "ms");
startTime = System.currentTimeMillis();
BigDecimal b = new BigDecimal("1.0");
for(int i = 0; i < 100000; i++){
b.add(b);
}
endTime = System.currentTimeMillis();
System.out.println("BigDecimal程序运行时间:" + (endTime - startTime) + "ms");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
编译和运行:
javac BigDecimalDemo4.java -encoding utf8
java BigDecimalDemo4
double程序运行时间:1ms
BigDecimal程序运行时间:6ms
2
3
4
可以看到,10 万次的情况下,BigDecimal 的时间是 double 类型的 6 倍。
注意:以上结果是在我本地电脑下运行的(Win10,i9),不同电脑的运行结果不一样。
什么时候不精确的值也可以呢?例如用户平均价格,店铺评分,用户经纬度等本身就没有精准值一说的推荐使用 double 或 float,写代码更方便,计算效率也高得多;
# 小结
BigDecimal 的原理和坑还是挺难理解的,希望大家理解原理后,反复阅读知识点并动手实践。