【Java踩坑笔记】【基础语法篇】03_BigDecimal用double构造?精度就这样丢了

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 浮点数的本质缺陷

doublefloat 遵循 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 条军规

  1. 禁止使用 double/float 做金额计算
  2. BigDecimal 构造优先用字符串,其次用 valueOf(double)
  3. 比较大小用 compareTo,别用 equals
  4. 除法必须指定精度和舍入模式
  5. 运算结果用 setScale 明确精度,避免隐式精度扩散
  6. 数据库 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 + 背后发生了什么?别让拼接拖垮性能 ------ 为什么循环里用 + 拼接字符串是性能杀手,以及编译器的优化边界在哪里。