Java 之 BigDecimal 使用填坑

BigDecimal 的概述

Java 在 java.math 包中提供了 API 类 BigDecimal,用于对超过 16 位有效位的数进行精确的运算。双精度浮点型变量 double 可以处理 16 位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。在一般情况下,对于那些不需要准确计算精度的数字,可以直接使用 Float 和 Double 处理,但是 Double.valueOf(String)Float.valueOf(String) 会丢失精度。所以在开发中,如果需要高精度的计算结果,则必须使用 BigDecimal 类来操作数据。BigDecimal 所创建的是对象,因此不能使用传统的 -×÷ 等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是 BigDecimal 的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。

BigDecimal 的舍入模式

舍入模式(Rounding Mode)是 BigDecimal 类中的静态变量,其中包括了八种常见的舍入规则,比如四舍五入法、银行家舍入法等。

  • ROUND_UP

    • 向远离零的方向舍入。舍弃非零部分,并将非零舍弃部分相邻的一位数字加一。
  • ROUND_DOWN

    • 向接近零的方向舍入。舍弃非零部分,同时不会非零舍弃部分相邻的一位数字加一,采取截取行为。
  • ROUND_CEILING

    • 向正无穷的方向舍入。如果为正数,舍入结果同 ROUND_UP 一致;如果为负数,舍入结果同 ROUND_DOWN 一致。
    • 特别注意,此模式不会减少数值大小。
  • ROUND_FLOOR

    • ` 向负无穷的方向舍入。如果为正数,舍入结果同 ROUND_DOWN 一致;如果为负数,舍入结果同 ROUND_UP 一致。
    • 特别注意,此模式不会增加数值大小。
  • ROUND_HALF_UP

    • 向 “最接近” 的数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。
    • 如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。
    • 这种模式也就是常说的 “四舍五入”。
  • ROUND_HALF_DOWN

    • 向 “最接近” 的数字舍入,如果与两个相邻数字的距离相等,则为向下舍入的舍入模式。
    • 如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。
    • 这种模式也就是常说的 “五舍六入”。
  • ROUND_HALF_EVEN

    • 如果舍弃部分左边的数字奇数,则舍入行为与 ROUND_HALF_UP 相同。
    • 如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。
    • 注意:在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。
    • 此舍入模式也称为 “银行家舍入法”,主要在美国使用。
    • 这种模式也就是常说的 “四舍六入五留双”。被舍位为 5 时有两种情况,如果前一位为奇数,则入位,否则舍去。
  • ROUND_UNNECESSARY

    • 断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出 ArithmeticException。

BigDecimal 的注意事项

  • 当使用 floatdouble 这些浮点数据类型时,会丢失精度。
  • 当 BigDecimal 使用除法时,商的结果需要指定舍入模式,否则会抛出 ArithmeticException 异常。比如 ROUND_HALF_UP 模式是 “四舍五入”,ROUND_HALF_DOWN 模式是 “五舍六入”。
  • 禁止使用构造方法 BigDecimal(double) 的方式将 double 值转换为 BigDecimal 对象,因为存在丢失精度的风险。推荐使用入参为 String 类型的构造方法,或者使用 BigDecimal.valueOf() 方法。
  • 等值比较应该使用 compareTo() 方法,而不是 equals() 方法。因为 equals() 方法会比较值和精度(1.0 与 1.00 比较的返回结果为 true),而 compareTo() 方法则会忽略精度。

阿里巴巴 Java 开发手册

第一条开发准则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FloatDemo {

public static void main(String[] args) {
// 浮点数之间的等值判断,基本数据类型不能使用 == 进行比较
float e = 0.1f;
System.out.println(0.1 == e);

// 浮点数之间的等值判断,基本数据类型不能使用 == 进行比较
float a = 0.9f - 0.8f;
float b = 0.5f - 0.4f;
System.out.println(a == b);

// 浮点数之间的等值判断,包装类型不能使用 equals() 方法进行比较
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
System.out.println(x.equals(y));
}

}

程序运行的输出结果:

1
2
3
false
false
false

第二条开发准则

1
2
3
4
5
6
7
8
9
10
11
12
public class BigDecimalDemo {

public static void main(String[] args) {
BigDecimal amount1 = new BigDecimal("0.9");
BigDecimal amount2 = new BigDecimal("0.90");

// BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。
System.out.println("equals() 比较结果:" + amount1.equals(amount2));
System.out.println("compareTo() 比较结构:" + amount1.compareTo(amount2));
}

}

程序运行的输出结果:

1
2
equals() 比较结果:false
compareTo() 比较结果:0

第三条开发准则

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
public class BigDecimalDemo {

public static void main(String[] args) {
// 禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象,但非要转呢?
BigDecimal amount1 = new BigDecimal(0.03);
BigDecimal amount2 = new BigDecimal(0.02);
// 实际存储的值发生了变化
System.out.println("amount1: " + amount1);
System.out.println("amount2: " + amount2);
// 应该等于 0.01,实际上却不是
System.out.println(amount1.subtract(amount2));

// 推荐使用入参为 String 的构造方法
BigDecimal amount3 = new BigDecimal("0.03");
BigDecimal amount4 = new BigDecimal("0.02");
// 应该等于 0.01
System.out.println(amount3.subtract(amount4));

// 使用 BigDecimal 的 valueOf() 方法
BigDecimal amount5 = BigDecimal.valueOf(0.03);
BigDecimal amount6 = BigDecimal.valueOf(0.02);
// 应该等于 0.01
System.out.println(amount5.subtract(amount6));
}

}

程序运行的输出结果:

1
2
3
4
5
amount1: 0.0299999999999999988897769753748434595763683319091796875
amount2: 0.0200000000000000004163336342344337026588618755340576171875
0.0099999999999999984734433411404097569175064563751220703125
0.01
0.01

其他开发准则

科学计数法的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BigDecimalDemo {

public static void main(String[] args) {
BigDecimal amount1 = BigDecimal.valueOf(1234567890123456789.3141592631415926);
System.out.println(amount1); // 使用科学计数法:1.23456789012345677E+18
System.out.println(amount1.toString()); // 使用科学计数法:1.23456789012345677E+18
System.out.println(amount1.toPlainString()); // 不使用科学计数法

System.out.println();

BigDecimal amount2 = new BigDecimal("1234567890123456789.3141592631415926");
System.out.println(amount2);
System.out.println(amount2.toString());
System.out.println(amount2.toPlainString());
}

}

程序运行的输出结果:

1
2
3
4
5
6
7
1.23456789012345677E+18
1.23456789012345677E+18
1234567890123456770

1234567890123456789.3141592631415926
1234567890123456789.3141592631415926
1234567890123456789.3141592631415926

除法需要设置舍入模式

当 BigDecimal 使用除法时,商的结果需要指定舍入模式,否则会抛出 ArithmeticException 异常。比如 ROUND_HALF_UP 模式是 “四舍五入”,ROUND_HALF_DOWN 模式是 “五舍六入”。

1
2
3
4
5
6
7
8
9
10
public class BigDecimalDemo {

public static void main(String[] args) {
BigDecimal amount1 = new BigDecimal("2.0");
BigDecimal amount2 = new BigDecimal("3.0");
// System.out.println(amount1.divide(amount2)); // 会抛出 ArithmeticException 异常
System.out.println(amount1.divide(amount2, 2, RoundingMode.HALF_UP)); // 这种模式也就是常说的 "四舍五入"
}

}

程序运行的输出结果:

1
0.67

如何选择数据库表字段的类型

在电商项目的支付 / 购物车模块中,往往会涉及到金额处理的操作,那么应该如何设计数据库表才能保证金额数据的精度呢?

  • 数据库表字段的类型可以选择 DECIMAL(M, D),其中 M 是数字的最大位数,包括整数和小数部分,而 D 是小数点右边的小数位数。比如:DECIMAL(10, 2) 可以存储最多 10 位数字,其中包括 2 位小数位,可以存储的范围是 -99999999.99 到 99999999.99。

  • POJO 的属性可以使用 BigDecimal 类型
1
2
@Schema(title = "交易金额")
private BigDecimal amount;

浮点数比较值相等

在不使用 BigDecimal 的情况下,浮点数不能使用 == 或者 equals() 方法来进行等值判断(示例代码如下)。这是因为浮点数采用 “尾数 + 阶码” 的编码方式,类似于科学计数法的 “有效数字 + 指数” 的表示方式。二进制无法精确表示大部分的十进制小数,具体原理可参考《码出高效》。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FloatDemo {

public static void main(String[] args) {
// 浮点数之间的等值判断,基本数据类型不能使用 == 进行比较
float e = 0.1f;
System.out.println(0.1 == e);

// 浮点数之间的等值判断,基本数据类型不能使用 == 进行比较
float a = 0.9f - 0.8f;
float b = 0.5f - 0.4f;
System.out.println(a == b);

// 浮点数之间的等值判断,包装类型不能使用 equals() 方法进行比较
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
System.out.println(x.equals(y));
}

}

程序运行的输出结果:

1
2
3
false
false
false

正确的写法应该是指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FloatDemo {

public static void main(String[] args) {
float a = 0.9f - 0.8f;
float b = 0.5f - 0.4f;
float diff = 1e-6f;

if (Math.abs(a - b) < diff) {
System.out.println("true");
}
}

}

程序运行的输出结果:

1
true

最佳编码实践

在电商项目的支付 / 购物车模块中,往往会涉及到金额处理的操作,那么可以直接使用下面的 BigDecimal 高精度数学运算工具类。

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import java.math.BigDecimal;

/**
* 用于高精度处理的常用数学运算
*/
public class ArithmeticUtils {

//默认除法运算精度
private static final int DEF_DIV_SCALE = 10;

/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/

public static double add(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}

/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static BigDecimal add(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2);
}

/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @param scale 保留scale 位小数
* @return 两个参数的和
*/
public static String add(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}

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

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

/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @param scale 保留scale 位小数
* @return 两个参数的差
*/
public static String sub(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}

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

/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static BigDecimal mul(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2);
}

/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static double mul(double v1, double v2, int scale) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return round(b1.multiply(b2).doubleValue(), scale);
}

/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static String mul(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}

/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/

public static double div(double v1, double v2) {
return div(v1, v2, DEF_DIV_SCALE);
}

/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示需要精确到小数点以后几位
* @return 两个参数的商
*/
public static String div(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v1);
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
}

/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(Double.toString(v));
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static String round(String v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(v);
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}

/**
* 取余数
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static String remainder(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}

/**
* 取余数 BigDecimal
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP);
}

/**
* 比较大小
*
* @param v1 被比较数
* @param v2 比较数
* @return 如果v1 大于v2 则 返回true 否则false
*/
public static boolean compare(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
int bj = b1.compareTo(b2);
boolean res;
if (bj > 0) {
res = true;
} else {
res = false;
}
return res;
}
}