Java项目日常开发中使用BigDecimal常见问题总结
Java项目中有计算精度要求高的场景(如金额计算)会使用BigDecimal
类型来代替Double
、Float
。
本文整理了一些日常开发中使用BigDecimal
值得注意的问题和代码实例。
BigDecimal
初始化时入参应使用String
类型
例1:
BigDecimal x = new BigDecimal(3.33);
BigDecimal y = new BigDecimal("3.33");
System.out.println("x=" + x);
System.out.println("y=" + y);
输出:
x=3.3300000000000000710542735760100185871124267578125
y=3.33
使用double
类型初始化的BigDecimal
对象,出现了精度丢失的情况,应使用String
类型值来初始化。
BigDecimal
初始化使用String
类型时,需注意字符串前后不要有空白符
例2:
BigDecimal a = new BigDecimal("102.7");
System.out.println(a);
BigDecimal b = null;
try {
b = new BigDecimal("102.7 ");
System.out.println(b);
} catch (Exception e) {
e.printStackTrace();
}
输出:
java.lang.NumberFormatException
at java.math.BigDecimal.<init>(BigDecimal.java:497)
at java.math.BigDecimal.<init>(BigDecimal.java:383)
at java.math.BigDecimal.<init>(BigDecimal.java:809)
...
构建BigDecimal
对象失败,抛NumberFormatException
。
如果字符串数值是第三方传入、mq消息、不小心人工输入空格等,需要注意这点。
BigDecimal
乘法运算结果格式化
例3:
BigDecimal c = new BigDecimal("13.14");
System.out.println(c.multiply(new BigDecimal("100")).toString());
输出:
1314.00
直接toString()
方法,输出结果在小数点后多了2位0,如果想保留整数,可使用toBigInteger()
方法。
// 1314
System.out.println(c.multiply(new BigDecimal("100")).toBigInteger());
BigDecimal
除法运算可能有除不尽的情况,应使用带scale参数的divide方法
例4:
try {
BigDecimal nonTerminatingDecimal = new BigDecimal("1").divide(new BigDecimal("3"));
System.out.println("nonTerminatingDecimal=" + nonTerminatingDecimal);
} catch (Exception e) {
// e.printStackTrace();
}
输出:
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1693)
...
因为1除3除不尽,结果是无限循环小数,因此抛了ArithmeticException
,应使用带scale参数的divide方法,如指定保留小数位数、舍入模式。
BigDecimal nonTerminatingDecimal2 = new BigDecimal("1").divide(new BigDecimal("3"), 2, RoundingMode.HALF_DOWN);
// 0.33
System.out.println(nonTerminatingDecimal2);
BigDecimal
做运算时注意次序问题
例5:
public static void main(String[] args) {
Integer totalCount;
Integer successCount;
Integer errorCount;
totalCount = 618;
successCount = 60;
errorCount = 18;
// 12.48 ERROR
calProgress1(totalCount, successCount, errorCount);
// 12.62 OK
calProgress2(totalCount, successCount, errorCount);
System.out.println(StringUtils.center("分隔线", 50, "-"));
totalCount = 61800;
successCount = 60;
errorCount = 18;
// 0.00 ERROR
calProgress1(totalCount, successCount, errorCount);
// 0.13 ERROR
calProgress2(totalCount, successCount, errorCount);
}
private static void calProgress1(Integer totalCount, Integer successCount, Integer errorCount) {
BigDecimal calProgress = new BigDecimal(String.valueOf(successCount + errorCount))
.multiply(new BigDecimal("100")
.divide(new BigDecimal(String.valueOf(totalCount)), 2, RoundingMode.HALF_UP));
System.out.println(calProgress.toString());
}
private static void calProgress2(Integer totalCount, Integer successCount, Integer errorCount) {
BigDecimal calProgress2 = new BigDecimal(String.valueOf(successCount + errorCount))
.multiply(new BigDecimal("100"))
.divide(new BigDecimal(String.valueOf(totalCount)), 2, RoundingMode.HALF_UP);
System.out.println(calProgress2.toString());
}
输出:
12.48
12.62
-----------------------分隔线------------------------
0.00
0.13
程序逻辑是计算百分比,公式:(成功数+失败数)*100/总数;
注意calProgress1
和calProgress2
里代码的差别:
calProgress1
方法里multiply(new BigDecimal("100")
少写了个)
,导致先做了100/总数
除法运算。
2个方法都能编译通过运行;
第1组输出由于数据量较小,结果差异不大,看上去都能计算出百分比,
第2组输出加大了总数量,calProgress1
输出结果为0.00
,丢失了部分精度。
例5是来自项目里的实例,某界面上展示数据处理的百分比进度,测试环境数据量较小,界面上看上去没问题,能从0-100%正常展示;
线上环境数据量较大,程序处理数据,界面上长时间展示0.00%。
修正为calProgress2
里的代码,保证严格按照公式计算,先做乘法最后除法运算,除法运算指定保留小数位数和舍入方式。