Java 科学运算之 BigDecimal

如果在资金计算中,还在使用 +-*\ 计算,我想你该离删库跑路不远了。

一、BigDecimal的产生背景

首先我们先来看如下代码示例:

1
2
3
4
5
6
7
@Test
public void countDemo() {
logger.info("result:{}", 0.06 + 0.01);
logger.info("result:{}", 1.0 - 0.42);
logger.info("result:{}", 4.015 * 100);
logger.info("result:{}", 303.1 / 1000);
}

结果如下

1
2
3
4
result:0.06999999999999999
result:0.5800000000000001
result:401.49999999999994
result:0.30310000000000004

问题在哪里呢?原因在于我们的计算机是二进制的,浮点数是没有办法用二进制进行精确表示

Javafloat只能用来进行科学计算或工程计算,在大多数的商业计算中,一般采用java.math.BigDecimal类来进行精确计算。

二、正确运用BigDecimal

2.1 BigDecimal常量

BigDecimal定义了几个常用的值,0110,静态的,可以通过类名直接引用如:BigDecimal.ZERO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* The value 0, with a scale of 0.
*
* @since 1.5
*/
public static final BigDecimal ZERO =
zeroThroughTen[0];

/**
* The value 1, with a scale of 0.
*
* @since 1.5
*/
public static final BigDecimal ONE =
zeroThroughTen[1];

/**
* The value 10, with a scale of 0.
*
* @since 1.5
*/
public static final BigDecimal TEN =
zeroThroughTen[10];

2.2 构造方法

BigDecimal4个常用构造方法

  1. new BigDecimal(int) 创建一个具有参数所指定整数值的对象;
  2. new BigDecimal(double) 创建一个具有参数所指定双精度值的对象;
  3. new BigDecimal(long) 创建一个具有参数所指定长整数值的对象;
  4. new BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象。

2.3 BigDecimal运算一般步骤

在使用BigDecimal类来进行计算的时候,主要三个步骤:

  1. float或者double变量构建BigDecimal对象。
  2. 通过调用BigDecimal的加,减,乘,除等相应的方法进行算术运算。
  3. BigDecimal对象转换成floatdoubleint等类型。

三、常用方法详解

在一般开发过程中,我们数据库中存储的数据都是floatdouble类型的。我封装了一个工具类,该工具类提供加、减、乘、除、向上取值、向下取值等运算。

3.1 普通加减乘除

1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* 默认除法运算精度
*/
private static final int DEF_DIV_SCALE = 10;

/**
* 精确的加法运算。
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(Double v1, Double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.add(b2).doubleValue();
}

/**
* 精确的减法运算。
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(Double v1, Double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.subtract(b2).doubleValue();
}

/**
* 精确的乘法运算。
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(Double v1, Double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}

/**
* (相对)精确的除法运算,当发生除不尽的情况时,默认精确到小数点以后10位,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double div(Double v1, Double v2) {
return div(v1, v2, DEF_DIV_SCALE, RoundingMode.HALF_UP);
}
```

### 3.2 按照精度四舍五入或者向上/向下取整

```java
/**
* 得到计算结果后四舍五入
*
* @param val
* @param scale 精度
* @return 例如保留三位小数:0.646464 =》 0.646
*/
public static double roundHalfUp(double val, int scale) {
BigDecimal dec = BigDecimal.valueOf(val);
return dec.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 得到计算结果后向上取整
*
* @param val val
* @param scale 精度
* @return 例如保留三位小数:0.646464 =》 0.647
*/
public static double roundUp(double val, int scale) {
BigDecimal dec = BigDecimal.valueOf(val);
return dec.setScale(scale, RoundingMode.UP).doubleValue();
}

/**
* 得到计算结果后向下取整
*
* @param val val
* @param scale 精度
* @return 例如保留三位小数:0.646464 =》 0.646
*/
public static double roundDown(double val, int scale) {
BigDecimal dec = BigDecimal.valueOf(val);
return dec.setScale(scale, RoundingMode.DOWN).doubleValue();
}

/**
* 除法运算加上向上取整。
*
* @param v1 被除数
* @param v2 除数
* @param scale 精度
* @return
*/
public static double divOfUp(Double v1, Double v2, int scale) {
return div(v1, v2, scale, RoundingMode.UP);
}

/**
* 除法运算加上向下取整。
*
* @param v1 被除数
* @param v2 除数
* @param scale 精度
* @return
*/
public static double divOfDown(Double v1, Double v2, int scale) {
return div(v1, v2, scale, RoundingMode.DOWN);
}

/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由 scale 参数指定精度,roundingMode 指定取舍方式。
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @param roundingMode 指定取舍方式。
* @return 两个参数的商
*/
private static double div(double v1, double v2, int scale, RoundingMode roundingMode) {
//如果精确范围小于0,抛出异常信息
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, roundingMode).doubleValue();
}

3.3 如果正好是除以或乘以整十

有时候,比如人民币的分/圆/万计算,我们正好是除以或者乘以百/万的计算,除了加法和减法,BigDecimal 提供直接移动小数点的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 小数点向右移动指定位数
*
* @param val 被乘数
* @param index 移动位数
* @return 例如向右移动四位小数:1000.01 =》 1000010.0
*/
public static double movePointRight(Double val, int index) {
BigDecimal value = BigDecimal.valueOf(val);
return value.movePointRight(index).doubleValue();
}

/**
* 小数点向左移动指定位数
*
* @param val 被除数
* @param index 移动位数
* @return 例如向左移动四位小数:1000.01 =》 1.00001
*/
public static double movePointLeft(Double val, int index) {
BigDecimal value = BigDecimal.valueOf(val);
return value.movePointLeft(index).doubleValue();
}

3.4 BigDecimal 大小比较

在比较两个BigDecimal的值是否相等时,必须使用compareTo()方法来比较,它根据两个值的大小分别返回-110,分别表示小于、大于和等于。

1
2
3
4
5
6
7
8
9
10
@Test
public void compareDecimal() {
BigDecimal v1 = BigDecimal.valueOf(1.21);
BigDecimal v2 = BigDecimal.valueOf(1.22);
BigDecimal v3 = BigDecimal.valueOf(1.22);
// -1:小于、1:大于、0:等于
logger.info("result:{}", v1.compareTo(v2));
logger.info("result:{}", v2.compareTo(v1));
logger.info("result:{}", v2.compareTo(v3));
}

3.5BigDecimal精度也丢失

仔细的你一定发现了,在工具类中使用BigDecimal中,都是使用BigDecimalvalueOf()创建对象的,而valueOf() 的内部实现是

1
2
3
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}

及构造器用的是new BigDecimal(String),因为其他的如new BigDecimal(int)/new BigDecimal(long)/new BigDecimal(double),还是会发生精度丢失的问题。

  • 示例
1
2
3
4
5
6
7
8
9
10
11
@Test
public void precisionLose() {
BigDecimal v1 = new BigDecimal(1.01);
BigDecimal v2 = new BigDecimal(1.02);
BigDecimal v3 = new BigDecimal("1.01");
BigDecimal v4 = new BigDecimal("1.02");
// 2.0300000000000000266453525910037569701671600341796875
logger.info("result:{}", v1.add(v2));
// 2.03
logger.info("result:{}", v3.add(v4));
}

四、总结

  1. BigDecimal用于表示精确的小数,常用于财务计算;

  2. 比较BigDecimal的值是否相等,必须使用compareTo()而不能使用equals()

【Github 示例代码】

更多 Java 笔记,详见【Java 知识笔记本】,欢迎提供想法建议。

Van wechat
最新文章,欢迎您扫一扫上面的微信公众号!
-------------    本文结束  感谢您的阅读    -------------