03 | BigDecimal 用 double 构造?精度就这样丢了
摘要:
new BigDecimal(0.1)得到的不是 0.1,而是 0.100000000000000005551… 本文讲清 BigDecimal 的正确使用方式。
一、问题现象
publicclassBigDecimalTest{publicstaticvoidmain(String[]args){BigDecimala=newBigDecimal(0.1);BigDecimalb=newBigDecimal("0.1");System.out.println(a);// 0.1000000000000000055511151231257827021181583404541015625System.out.println(b);// 0.1System.out.println(a.equals(b));// false}}再看一个金融场景的灾难:
publicclassMoneyCalc{publicstaticvoidmain(String[]args){BigDecimalprice=newBigDecimal(19.99);// ❌ 用 double 构造BigDecimalquantity=newBigDecimal("3");BigDecimaltotal=price.multiply(quantity);System.out.println(total);// 59.970000000000001... (不是 59.97!)}}二、踩坑现场
场景 1:金额计算用了double构造
// ❌ 错误:电商订单金额计算BigDecimalorderAmount=newBigDecimal(199.99);BigDecimaltaxRate=newBigDecimal(0.06);BigDecimaltax=orderAmount.multiply(taxRate);// 预期:11.9994,实际:各种奇怪的精度问题场景 2:equals比较不忽略精度差异
BigDecimala=newBigDecimal("1.0");BigDecimalb=newBigDecimal("1.00");System.out.println(a.equals(b));// false!精度不同System.out.println(a.compareTo(b)==0);// true,正确场景 3:除法不设置舍入模式
BigDecimala=newBigDecimal("10");BigDecimalb=newBigDecimal("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) | 内部先转成字符串再解析 | ✅ 推荐 |
// BigDecimal.valueOf() 源码publicstaticBigDecimalvalueOf(doubleval){returnnewBigDecimal(Double.toString(val));// 先转字符串,再构造}3.3equalsvscompareTo
// equals 比较:值 + 精度(scale)完全一致才返回 trueBigDecimala=newBigDecimal("1.0");// scale = 1BigDecimalb=newBigDecimal("1.00");// scale = 2a.equals(b)// false:scale 不同// compareTo 比较:只比较数值大小,忽略精度a.compareTo(b)==0// true:数值相等结论:比较数值大小时用compareTo,不要用equals。
3.4 八种舍入模式
// BigDecimal 除法必须指定舍入模式BigDecimala=newBigDecimal("10");BigDecimalb=newBigDecimal("3");// ✅ 正确写法BigDecimalresult=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 金额计算:永远用字符串构造
// ✅ 正确写法BigDecimalprice=newBigDecimal("19.99");BigDecimalquantity=newBigDecimal("3");BigDecimaltotal=price.multiply(quantity);System.out.println(total);// 59.97(精确)4.2 从double转换:用valueOf
// ✅ 如果源头就是 double(比如第三方接口返回)doublevalue=19.99;BigDecimalbd=BigDecimal.valueOf(value);// 内部会先做 Double.toString()4.3 比较大小:用compareTo
BigDecimala=newBigDecimal("1.0");BigDecimalb=newBigDecimal("1.00");// ✅ 正确比较if(a.compareTo(b)==0){System.out.println("相等");}4.4 完整工具类示例
importjava.math.BigDecimal;importjava.math.RoundingMode;publicclassMoneyUtils{/** 默认精度(金额用2位) */privatestaticfinalintDEFAULT_SCALE=2;/** 金额相加 */publicstaticBigDecimaladd(Stringv1,Stringv2){returnnewBigDecimal(v1).add(newBigDecimal(v2));}/** 金额相减 */publicstaticBigDecimalsubtract(Stringv1,Stringv2){returnnewBigDecimal(v1).subtract(newBigDecimal(v2));}/** 金额相乘,保留2位小数 */publicstaticBigDecimalmultiply(Stringv1,Stringv2){returnnewBigDecimal(v1).multiply(newBigDecimal(v2)).setScale(DEFAULT_SCALE,RoundingMode.HALF_UP);}/** 金额相除,保留2位小数 */publicstaticBigDecimaldivide(Stringv1,Stringv2){returnnewBigDecimal(v1).divide(newBigDecimal(v2),DEFAULT_SCALE,RoundingMode.HALF_UP);}/** 比较相等(忽略精度差异) */publicstaticbooleanequals(BigDecimalv1,BigDecimalv2){returnv1.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 + 背后发生了什么?别让拼接拖垮性能—— 为什么循环里用+拼接字符串是性能杀手,以及编译器的优化边界在哪里。