BigDecimal 是 Java 中用于精确表示和计算十进制数的类,它位于 java.math 包下。为什么需要它? 因为 Java 中基本类型 float 和 double,以及它们的包装类 Float 和 Double,都是基于二进制浮点数实现的,这在处理十进制数时会产生精度丢失问题。

为什么 float 和 double 不够用?
计算机底层使用二进制(0和1)来表示所有数据,十进制小数(如 1)在转换为二进制时,是一个无限循环小数。
示例:
public class DoubleProblem {
public static void main(String[] args) {
// 期望结果是 0.3
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 输出: 0.30000000000000004
// 期望结果是 1.0
double c = 1.00;
double d = 0.99;
System.out.println(c - d); // 输出: 0.010000000000000009
}
}
从上面的例子可以看出,简单的加减法都可能导致精度错误,这在金融、会计等对精度要求极高的领域是绝对不能接受的。
BigDecimal 通过将数字表示为“未缩放整数”和“缩放因子”(即小数点位置)的虚拟组合,完美地解决了这个问题。1 会被存储为 1 和 10(即 1 * 10^-1),2 会被存储为 2 和 10,这样就能精确表示。

BigDecimal 的核心构造方法
创建 BigDecimal 实例时,强烈建议使用 String 参数的构造方法,以避免精度问题。
✅ 推荐的构造方法
// 1. 使用 String 构造,这是最精确、最安全的方式
BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = new BigDecimal("123456789.987654321");
// 2. 使用 int 或 long 构造
BigDecimal bd3 = new BigDecimal(123); // 整数
BigDecimal bd4 = new BigDecimal(123L); // 长整数
❌ 避免使用的构造方法
// 错误示范!不要使用 double 构造! // 0.1 这个 double 本身就不精确,所以传给 BigDecimal 的也是不精确的值 BigDecimal wrongBd1 = new BigDecimal(0.1); // 内部存储的不是 0.1,而是 0.1000000000000000055511151231257827021181583404541015625 BigDecimal wrongBd2 = new BigDecimal(0.2); // 同理 System.out.println(wrongBd1.add(wrongBd2)); // 输出不是精确的 0.3
其他创建方式:valueOf()
BigDecimal 类提供了一个静态工厂方法 valueOf(),它是创建 BigDecimal 实例的推荐方式之一,因为它内部做了优化。
// 使用 valueOf() 方法 // 对于 double 类型,它会先将 double 转换为 String,再构造 BigDecimal // 这比 new BigDecimal(double) 要精确得多 BigDecimal safeBd1 = BigDecimal.valueOf(0.1); BigDecimal safeBd2 = BigDecimal.valueOf(0.2); System.out.println(safeBd1.add(safeBd2)); // 输出: 0.3
- 首选:
BigDecimal.valueOf(double)或new BigDecimal(String) - 次选:
new BigDecimal(int/long) - 避免:
new BigDecimal(double)
常用方法
BigDecimal 提供了丰富的算术运算、比较和格式化方法。
算术运算
所有算术运算方法都会返回一个新的 BigDecimal 对象,它们不会修改原始对象(BigDecimal 是不可变的)。
| 方法 | 描述 | 示例 |
|---|---|---|
add(BigDecimal augend) |
加法 | bd1.add(bd2) |
subtract(BigDecimal subtrahend) |
减法 | bd1.subtract(bd2) |
multiply(BigDecimal multiplicand) |
乘法 | bd1.multiply(bd2) |
divide(BigDecimal divisor) |
除法 | bd1.divide(bd2) |
divide(BigDecimal divisor, int scale, RoundingMode roundingMode) |
除法(指定精度和舍入模式) | bd1.divide(bd2, 2, RoundingMode.HALF_UP) |
除法特别注意:
普通的 divide 方法可能会抛出 ArithmeticException,因为除不尽(如 1/3),通常需要指定保留小数位数和舍入模式。
BigDecimal num1 = new BigDecimal("1");
BigDecimal num2 = new BigDecimal("3");
// 错误示范,会抛出异常 ArithmeticException
// num1.divide(num2);
// 正确示范:保留2位小数,四舍五入
BigDecimal result = num1.divide(num2, 2, RoundingMode.HALF_UP);
System.out.println(result); // 输出: 0.33
比较方法
| 方法 | 描述 |
|---|---|
compareTo(BigDecimal val) |
比较两个值的大小,返回 -1 (小于), 0 (等于), 1 (大于)。 |
equals(Object x) |
比较两个 BigDecimal 的值和精度是否都相等。new BigDecimal("1.00").equals(new BigDecimal("1")) 返回 false。 |
intValue(), longValue(), doubleValue() |
转换为基本类型,注意可能丢失精度。 |
BigDecimal bdA = new BigDecimal("10.00");
BigDecimal bdB = new BigDecimal("10.0");
BigDecimal bdC = new BigDecimal("9");
System.out.println(bdA.compareTo(bdB)); // 输出: 0 (值相等)
System.out.println(bdA.equals(bdB)); // 输出: false (精度不同)
System.out.println(bdA.compareTo(bdC)); // 输出: 1
System.out.println(bdC.compareTo(bdA)); // 输出: -1
精度和舍入方法
| 方法 | 描述 |
|---|---|
setScale(int scale, RoundingMode roundingMode) |
设置小数位数,并指定舍入模式,这是最常用的舍入方法。 |
setScale(int scale) |
设置小数位数,使用默认的舍入模式(RoundingMode.UNNECESSARY,表示不允许舍入,否则报错)。 |
round(MathContext mc) |
根据指定的精度和舍入模式进行舍入。 |
BigDecimal price = new BigDecimal("99.899");
// 保留2位小数,四舍五入
BigDecimal roundedPrice = price.setScale(2, RoundingMode.HALF_UP);
System.out.println(roundedPrice); // 输出: 99.90
// 保留1位小数,向上取整(进一法)
BigDecimal roundedPriceUp = price.setScale(1, RoundingMode.CEILING);
System.out.println(roundedPriceUp); // 输出: 99.9
舍入模式
BigDecimal 的舍入行为由 java.math.RoundingMode 枚举定义,最常用的是 HALF_UP(四舍五入)。
| 舍入模式 | 描述 |
|---|---|
UP |
远离零方向舍入。绝对值变大。 (5.5 -> 6, -5.5 -> -6) |
DOWN |
向零方向舍入。绝对值变小。 (5.5 -> 5, -5.5 -> -5) |
CEILING |
向正无穷方向舍入。 (5.5 -> 6, -5.5 -> -5) |
FLOOR |
向负无穷方向舍入。 (5.5 -> 5, -5.5 -> -6) |
HALF_UP |
四舍五入 (最常用)。 (5.5 -> 6, 5.4 -> 5) |
HALF_DOWN |
五舍六入。 (5.5 -> 5, 5.6 -> 6) |
HALF_EVEN |
银行家舍入法,如果舍弃部分的前一位是奇数,则向上舍入;如果是偶数,则向下舍入,这是最精确、最常用的舍入策略,可以最大程度地减少累积误差。 (5.5 -> 6, 6.5 -> 6, 2.5 -> 2) |
最佳实践
-
永远不要使用
double或float来构造BigDecimal。// 错误 BigDecimal price = new BigDecimal(99.99); // 正确 BigDecimal price = new BigDecimal("99.99"); // 或者 BigDecimal price = BigDecimal.valueOf(99.99); -
总是为除法操作指定舍入模式,否则程序可能会在运行时崩溃。
// 错误 // result = a.divide(b); // 正确 result = a.divide(b, 2, RoundingMode.HALF_UP);
-
使用
compareTo()进行比较,而不是equals()。equals()会比较值和精度(小数位数)。compareTo()只比较值的大小,这在业务逻辑中(如比较价格)通常才是我们想要的。
-
在循环中进行累加时,优先使用
add方法,而不是 。// 错误示范:将 BigDecimal 转为 double 进行计算,会丢失精度 double sum = 0.0; for (int i = 0; i < 10; i++) { sum += 0.1; } System.out.println(sum); // 输出不是 1.0 // 正确示范:使用 BigDecimal 的 add 方法 BigDecimal bdSum = BigDecimal.ZERO; for (int i = 0; i < 10; i++) { bdSum = bdSum.add(BigDecimal.valueOf(0.1)); } System.out.println(bdSum); // 输出 1.0
完整示例
下面是一个模拟购物车计算总价的完整示例,涵盖了 BigDecimal 的主要用法。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class BigDecimalExample {
public static void main(String[] args) {
// 商品列表:名称、单价、数量
String[][] products = {
{"苹果", "5.99", "2"},
{"香蕉", "3.49", "1.5"}, // 1.5公斤
{"牛奶", "12.50", "1"}
};
// 1. 初始化总金额,使用 String 构造确保初始值精确
BigDecimal totalAmount = new BigDecimal("0.00");
// 2. 遍历商品列表,计算小计并累加到总金额
for (String[] product : products) {
String name = product[0];
BigDecimal price = new BigDecimal(product[1]);
BigDecimal quantity = new BigDecimal(product[2]);
// 计算单项小计
BigDecimal subtotal = price.multiply(quantity);
System.out.printf("商品: %s, 小计: %s%n", name, subtotal);
// 累加到总金额
totalAmount = totalAmount.add(subtotal);
}
// 3. 计算税金(假设税率为 8%)
BigDecimal taxRate = new BigDecimal("0.08");
BigDecimal tax = totalAmount.multiply(taxRate).setScale(2, RoundingMode.HALF_UP);
System.out.printf("税金: %s%n", tax);
// 4. 计算最终应付金额(总金额 + 税金)
BigDecimal finalAmount = totalAmount.add(tax);
// 5. 格式化输出,保留两位小数
System.out.println("------------------------");
System.out.printf("商品总额: %s%n", totalAmount.setScale(2, RoundingMode.HALF_UP));
System.out.printf("应付税金: %s%n", tax);
System.out.printf("应付总额: %s%n", finalAmount.setScale(2, RoundingMode.HALF_UP));
}
}
输出结果:
商品: 苹果, 小计: 11.98
商品: 香蕉, 小计: 5.235
商品: 牛奶, 小计: 12.5
税金: 2.38
------------------------
商品总额: 29.72
应付税金: 2.38
应付总额: 32.10
BigDecimal 是 Java 进行精确十进制计算的基石,虽然它比基本类型使用起来更繁琐(需要创建新对象、处理舍入等),但在金融、电商、科学计算等任何不能容忍精度错误的场景下,它是唯一正确的选择,掌握它的构造、运算、比较和舍入规则,是每个 Java 开发者的必备技能。
