03 | BigDecimal 用 double 构造?精度就这样丢了
摘要 :
new BigDecimal(0.1)得到的不是 0.1,而是 0.100000000000000005551... 本文讲清 BigDecimal 的正确使用方式。
一、问题现象
java
public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal a = new BigDecimal(0.1);
BigDecimal b = new BigDecimal("0.1");
System.out.println(a); // 0.1000000000000000055511151231257827021181583404541015625
System.out.println(b); // 0.1
System.out.println(a.equals(b)); // false
}
}
再看一个金融场景的灾难:
java
public class MoneyCalc {
public static void main(String[] args) {
BigDecimal price = new BigDecimal(19.99); // ❌ 用 double 构造
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity);
System.out.println(total); // 59.970000000000001... (不是 59.97!)
}
}
二、踩坑现场
场景 1:金额计算用了 double 构造
java
// ❌ 错误:电商订单金额计算
BigDecimal orderAmount = new BigDecimal(199.99);
BigDecimal taxRate = new BigDecimal(0.06);
BigDecimal tax = orderAmount.multiply(taxRate);
// 预期:11.9994,实际:各种奇怪的精度问题
场景 2:equals 比较不忽略精度差异
java
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.equals(b)); // false!精度不同
System.out.println(a.compareTo(b) == 0); // true,正确
场景 3:除法不设置舍入模式
java
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
System.out.println(a.divide(b)); // ❌ ArithmeticException: Non-terminating decimal expansion
三、原理解析
3.1 浮点数的本质缺陷
double 和 float 遵循 IEEE 754 标准 ,用二进制表示十进制小数,而很多十进制小数无法用二进制精确表示。
十进制 0.1
= 二进制 0.00011001100110011...(无限循环)
= 计算机存储:0.1000000000000000055511...
new BigDecimal(double) 完全保留了这个不精确值,把它"精确"地存了下来。
3.2 三个构造方法对比
| 构造方法 | 行为 | 推荐度 |
|---|---|---|
new BigDecimal(double) |
保留 double 的全部不精确位 | ❌ 禁止使用 |
new BigDecimal(String) |
精确解析字符串 | ✅ 强烈推荐 |
BigDecimal.valueOf(double) |
内部先转成字符串再解析 | ✅ 推荐 |
java
// BigDecimal.valueOf() 源码
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val)); // 先转字符串,再构造
}
3.3 equals vs compareTo
java
// equals 比较:值 + 精度(scale)完全一致才返回 true
BigDecimal a = new BigDecimal("1.0"); // scale = 1
BigDecimal b = new BigDecimal("1.00"); // scale = 2
a.equals(b) // false:scale 不同
// compareTo 比较:只比较数值大小,忽略精度
a.compareTo(b) == 0 // true:数值相等
结论 :比较数值大小时用 compareTo,不要用 equals。
3.4 八种舍入模式
java
// BigDecimal 除法必须指定舍入模式
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// ✅ 正确写法
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);
System.out.println(result); // 3.33
常用舍入模式:
| 模式 | 说明 | 场景 |
|---|---|---|
RoundingMode.HALF_UP |
四舍五入 | 金额计算(最常用) |
RoundingMode.HALF_DOWN |
五舍六入 | 统计学 |
RoundingMode.HALF_EVEN |
银行家舍入法 | 金融专业场景 |
RoundingMode.DOWN |
直接截断 | 不四舍五入 |
RoundingMode.UP |
只要有小数就进位 | 保守计算 |
四、正确写法
4.1 金额计算:永远用字符串构造
java
// ✅ 正确写法
BigDecimal price = new BigDecimal("19.99");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity);
System.out.println(total); // 59.97(精确)
4.2 从 double 转换:用 valueOf
java
// ✅ 如果源头就是 double(比如第三方接口返回)
double value = 19.99;
BigDecimal bd = BigDecimal.valueOf(value); // 内部会先做 Double.toString()
4.3 比较大小:用 compareTo
java
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
// ✅ 正确比较
if (a.compareTo(b) == 0) {
System.out.println("相等");
}
4.4 完整工具类示例
java
import java.math.BigDecimal;
import java.math.RoundingMode;
public class MoneyUtils {
/** 默认精度(金额用2位) */
private static final int DEFAULT_SCALE = 2;
/** 金额相加 */
public static BigDecimal add(String v1, String v2) {
return new BigDecimal(v1).add(new BigDecimal(v2));
}
/** 金额相减 */
public static BigDecimal subtract(String v1, String v2) {
return new BigDecimal(v1).subtract(new BigDecimal(v2));
}
/** 金额相乘,保留2位小数 */
public static BigDecimal multiply(String v1, String v2) {
return new BigDecimal(v1)
.multiply(new BigDecimal(v2))
.setScale(DEFAULT_SCALE, RoundingMode.HALF_UP);
}
/** 金额相除,保留2位小数 */
public static BigDecimal divide(String v1, String v2) {
return new BigDecimal(v1)
.divide(new BigDecimal(v2), DEFAULT_SCALE, RoundingMode.HALF_UP);
}
/** 比较相等(忽略精度差异) */
public static boolean equals(BigDecimal v1, BigDecimal v2) {
return v1.compareTo(v2) == 0;
}
}
五、最佳实践
✅ 金额计算的 6 条军规
- 禁止使用
double/float做金额计算 BigDecimal构造优先用字符串,其次用valueOf(double)- 比较大小用
compareTo,别用equals - 除法必须指定精度和舍入模式
- 运算结果用
setScale明确精度,避免隐式精度扩散 - 数据库 Decimal 类型对应 Java
BigDecimal,不要用Double接收
🔍 数据库字段类型对照
| 数据库类型 | Java 类型 | 说明 |
|---|---|---|
DECIMAL(10,2) |
BigDecimal |
✅ 正确 |
DECIMAL(10,2) |
Double |
❌ 精度丢失 |
FLOAT / DOUBLE |
Double |
⚠️ 仅限非金融场景 |
🛠️ 阿里巴巴 Java 开发手册规约
【强制】 禁止使用构造方法
BigDecimal(double)的方式把 double 值转化为 BigDecimal 对象。优先推荐入参为 String 的构造方法,或使用 BigDecimal 的
valueOf方法。
六、小结
double本身不精确,new BigDecimal(double)会把这种不精确"精确"地保留下来- 正确构造方式 :
new BigDecimal(String)或BigDecimal.valueOf(double) equals比较精度,compareTo比较数值,金额比较用后者- 除法不设置舍入模式会抛
ArithmeticException - 金融场景全程用
BigDecimal,精度用setScale显式控制
下一篇预告:String + 背后发生了什么?别让拼接拖垮性能 ------ 为什么循环里用 + 拼接字符串是性能杀手,以及编译器的优化边界在哪里。