Java的BigDecimal类
一、精度丢失问题
先来看下面的这段浮点数运算的代码:
//0.1 + 0.2 不等于 0.3
System.out.println(0.1 + 0.2); // 输出:0.30000000000000004
我们发现输出结果并不是我们所预想的 0.3,这是因为计算机是采用二进制进行运算的,但是计算机可以表示的二进制的宽度是有限的,许多十进制小数转为二进制之后会无限循环,没办法完整表示,只能舍入为有限位数,所以会带来精度丢失的问题。
二、BigDecimal的使用
BigDecimal 是 Java 中用于高精度十进制运算的类,位于 java.math 包。它是金融、电商、计费系统中绝对不可或缺的核心工具。下面我们来看看具体如何使用:
一、创建实例
需要注意的是,不要使用 new BigDecimal(double),因为 double 本身已经失真了,传进去只会得到一个“失真后的垃圾值”。
BigDecimal d1 = new BigDecimal(0.1);//0.1000000000000000055511151231257827021181583404541015625
推荐使用 String 构造器或 BigDecimal.valueOf()
BigDecimal d2 = new BigDecimal("0.1");
BigDecimal d3 = BigDecimal.valueOf(0.1);
二、核心运算
public class Main {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("10.00");
BigDecimal b = new BigDecimal("3.00");
BigDecimal sum = a.add(b); // 加法:13.00
BigDecimal diff = a.subtract(b); // 减法:7.00
BigDecimal mul = a.multiply(b); // 乘法:30.0000 (精度会变多)
BigDecimal div = a.divide(b, 2, RoundingMode.HALF_UP); // 除法:3.33 (必须指定精度和舍入)
BigDecimal rem = a.remainder(b); // 取余:1.00
}
}
这里有两个问题需要注意:首先是乘法运算,我们发现最终的运算结果精度会变多,这是因为 BigDecimal 的乘法规则是“精度(小数位数)相加”,而不是像加减法那样“精度对齐”。我们先来看一下 BigDecimal的底层构成:
int scale: 小数位数 (即小数点后有几位)。BigInteger intVal: 去除小数点后的整数值 (有效数字)。
它的实际值计算公式是:intVal × 10^(-scale)
举例:
10.00存储为:intVal = 1000,scale = 2(即 1000 × 10⁻²)3.00存储为:intVal = 300,scale = 2(即 300 × 10⁻²)
做乘法时,Java 遵循基本的数学算术规则: 两个小数相乘,结果的小数位数等于两个乘数小数位数之和 。
即:结果scale = 乘数1.scale + 乘数2.scale
拿上面的例子代入计算:
10.00(scale=2) ×3.00(scale=2)- 底层计算:
1000 × 300 = 300000 - 结果scale:
2 + 2 = 4 - 最终结果:
300000 × 10⁻⁴=30.0000(精度从2位变成了4位)
如果希望去除多余的末尾 0,统一保留 2 位小数,我们可以在运算结束后,调用 setScale() 进行归一化处理。
BigDecimal a = new BigDecimal("10.00");
BigDecimal b = new BigDecimal("3.00");
BigDecimal result = a.multiply(b); // 结果是 30.0000
// 归一化:保留2位小数,四舍五入
BigDecimal normalized = result.setScale(2, RoundingMode.HALF_UP);
System.out.println(normalized); // 输出 30.00
如果你就是想彻底去掉末尾所有无意义的 0(比如 30.0000 -> 30),可以用 result.stripTrailingZeros(),它会返回精度最小的 BigDecimal。
//注意:当数值非常大或非常小时,toString() 会返回科学计数法表示,我们可以使用toPlainString()转为纯字符串
System.out.println(result.stripTrailingZeros());//3E+1
System.out.println(result.stripTrailingZeros().toPlainString());//30
第二个就是除法运算,我们上面指定了精度和舍入,这是因为如果不指定的话,divide 在遇到无限循环小数(如 10/3)时会直接抛出 ArithmeticException 。
常用的舍入模式:
HALF_UP:四舍五入。HALF_DOWN:五舍六入。HALF_EVEN:四舍六入五成双(银行家舍入法,用于减少误差累积,美国会计准则常用)。UP/DOWN:远离零 / 趋向零(相当于绝对值向上/向下)。
三、比较大小
在进行大小比较的时候,需要注意如下区别:
equals:比较值 + 精度(小数位数) 。1.0和1.00会被判定为不相等.compareTo:只比较数值大小 。1.0和1.00数值相等,返回0。
BigDecimal x = new BigDecimal("1.0");
BigDecimal y = new BigDecimal("1.00");
System.out.println(x.equals(y)); // false(因为精度不同)
System.out.println(x.compareTo(y)); // 0(表示数值相等)
四、注意事项
BigDecimal是不可变对象(Immutable),每一次运算都会返回一个新的BigDecimal,原对象不变。- 使用
String构造器 ,绝不用double构造器。 - 做除法时,必须指定精度和舍入模式 。
- 比较数值用
compareTo。 - 复用静态常量 :对于
0、1、10、100等常用值,使用BigDecimal.ZERO、BigDecimal.ONE、BigDecimal.TEN,避免重复new对象。 - 累加优化 :如果需要对大量数据进行求和,不要用
BigDecimal做循环add(会产生大量临时对象)。建议先用BigDecimal[]收集,最后统一累加;或者转为double先做粗略计算后再转回(仅限非金融严格场景)。

浙公网安备 33010602011771号