BigDecimal精确计算完全指南

BigDecimal精确计算完全指南

适合对象:Java开发者(初级至中级)


引言

在开始之前,我想问一个问题:0.1 + 0.2 等于多少?

你可能会说:这还不简单,当然是0.3!

但是,让我们在Java中验证一下:


public class Test {
    public static void main(String[] args) {
        double result = 0.1 + 0.2;
        System.out.println(result);
        System.out.println(result == 0.3);
    }
}

运行结果:


0.30000000000000004
false

看到了吗?计算机告诉我们,0.1 + 0.2 并不等于0.3!

这就是我们今天要解决的问题:如何在Java中进行精确的小数计算


第一章:为什么需要BigDecimal

1.1 浮点数的精度问题

首先,我们需要理解为什么
double

float
会有精度问题。

原理解释:

计算机使用二进制来存储数字,但很多十进制小数无法用二进制精确表示。就像十进制的1/3(0.3333…)无法用有限位数表示一样,十进制的0.1在二进制中也是无限循环小数。


十进制 0.1 → 二进制 0.0001100110011001100110011001100110011...(无限循环)

由于计算机存储空间有限,只能截断,因此产生了精度误差。

实际测试:


public class FloatPrecisionTest {
    public static void main(String[] args) {
        // 测试1:简单加法
        System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));

        // 测试2:连续加法
        double sum = 0.0;
        for (int i = 0; i < 10; i++) {
            sum += 0.1;
        }
        System.out.println("0.1 累加10次 = " + sum);
        System.out.println("是否等于1.0? " + (sum == 1.0));

        // 测试3:金额计算
        double price = 0.03;
        double quantity = 3;
        double total = price * quantity;
        System.out.println("0.03 × 3 = " + total);
    }
}

输出结果:


0.1 + 0.2 = 0.30000000000000004
0.1 累加10次 = 0.9999999999999999
是否等于1.0? false
0.03 × 3 = 0.09000000000000001

1.2 在金融系统中的风险

想象一下,如果你在开发一个银行系统,用户有1000元存款,年利率3%:


// ❌ 使用double计算利息
double principal = 1000.0;
double rate = 0.03;
double interest = principal * rate;
System.out.println("利息:" + interest);  // 30.000000000000004

// 如果要入库,四舍五入后可能丢失精度
// 如果有百万用户,累计误差会很大!

结论: 在涉及金额、数量等需要精确计算的场景,绝对不能使用float或double,必须使用
BigDecimal


第二章:BigDecimal的正确使用方法

2.1 创建BigDecimal对象

重要原则:永远不要用double来构造BigDecimal!


public class BigDecimalCreation {
    public static void main(String[] args) {
        // ❌ 错误方式1:用double构造
        BigDecimal wrong = new BigDecimal(0.1);
        System.out.println("用double构造:" + wrong);
        // 输出:0.1000000000000000055511151231257827021181583404541015625
        // 为什么?因为0.1的double表示本身就不精确!

        // ✅ 正确方式1:用String构造(推荐)
        BigDecimal correct1 = new BigDecimal("0.1");
        System.out.println("用String构造:" + correct1);
        // 输出:0.1

        // ✅ 正确方式2:用BigDecimal.valueOf()
        BigDecimal correct2 = BigDecimal.valueOf(0.1);
        System.out.println("用valueOf:" + correct2);
        // 输出:0.1
        // valueOf内部会先将double转为String,再构造

        // ✅ 正确方式3:整数可以直接构造
        BigDecimal correct3 = new BigDecimal(100);
        System.out.println("用int构造:" + correct3);
        // 输出:100
    }
}

总结:

最佳实践:用
String
构造 →
new BigDecimal("0.1")
可以接受:用
valueOf()

BigDecimal.valueOf(0.1)
绝不使用:用
double
构造 →
new BigDecimal(0.1)

2.2 四则运算

BigDecimal不能直接用
+

-

*

/
运算符,必须调用方法。


public class BigDecimalArithmetic {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("10.5");
        BigDecimal b = new BigDecimal("3.2");

        // 加法
        BigDecimal sum = a.add(b);
        System.out.println("加法:" + a + " + " + b + " = " + sum);

        // 减法
        BigDecimal diff = a.subtract(b);
        System.out.println("减法:" + a + " - " + b + " = " + diff);

        // 乘法
        BigDecimal product = a.multiply(b);
        System.out.println("乘法:" + a + " × " + b + " = " + product);

        // 除法(重点!)
        BigDecimal quotient = a.divide(b, 2, RoundingMode.HALF_UP);
        System.out.println("除法:" + a + " ÷ " + b + " = " + quotient);
    }
}

输出:


加法:10.5 + 3.2 = 13.7
减法:10.5 - 3.2 = 7.3
乘法:10.5 × 3.2 = 33.60
除法:10.5 ÷ 3.2 = 3.28

注意事项:

BigDecimal是不可变类,每次运算都会返回新对象原对象不会被修改除法必须特别注意(下一节详解)

2.3 除法的陷阱与解决

问题演示:


public class DivisionTrap {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("10");
        BigDecimal b = new BigDecimal("3");

        // ❌ 这样会抛异常!
        try {
            BigDecimal result = a.divide(b);
        } catch (ArithmeticException e) {
            System.out.println("异常:" + e.getMessage());
            // Non-terminating decimal expansion; no exact representable decimal result.
        }
    }
}

为什么会抛异常?

因为10 ÷ 3 = 3.33333…(无限循环小数),BigDecimal无法用有限位数精确表示,所以抛出异常。

正确的做法:


public class DivisionCorrect {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("10");
        BigDecimal b = new BigDecimal("3");

        // ✅ 方式1:指定精度和舍入模式
        BigDecimal result1 = a.divide(b, 2, RoundingMode.HALF_UP);
        System.out.println("保留2位小数,四舍五入:" + result1);  // 3.33

        // ✅ 方式2:使用MathContext
        MathContext mc = new MathContext(10, RoundingMode.HALF_UP);
        BigDecimal result2 = a.divide(b, mc);
        System.out.println("保留10位有效数字:" + result2);  // 3.333333333

        // ✅ 方式3:整除(返回商和余数)
        BigDecimal[] divideAndRemainder = a.divideAndRemainder(b);
        System.out.println("商:" + divideAndRemainder[0]);  // 3
        System.out.println("余数:" + divideAndRemainder[1]);  // 1
    }
}

常用的舍入模式(RoundingMode):

模式 说明 示例(保留1位)

HALF_UP
四舍五入(推荐) 1.25 → 1.3, 1.24 → 1.2

HALF_DOWN
五舍六入 1.25 → 1.2, 1.26 → 1.3

UP
向上取整(远离零) 1.21 → 1.3, -1.21 → -1.3

DOWN
向下取整(趋向零) 1.29 → 1.2, -1.29 → -1.2

CEILING
向正无穷方向取整 1.21 → 1.3, -1.21 → -1.2

FLOOR
向负无穷方向取整 1.29 → 1.2, -1.29 → -1.3

推荐: 金融计算通常使用
RoundingMode.HALF_UP
(四舍五入)

2.4 比较大小

重要:BigDecimal比较不能用 == 或 equals()!


public class BigDecimalComparison {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("1.00");

        // ❌ 错误方式1:使用 ==
        System.out.println("a == b: " + (a == b));  // false(比较引用)

        // ❌ 错误方式2:使用 equals()
        System.out.println("a.equals(b): " + a.equals(b));  // false!
        // 为什么?因为equals()会比较scale(精度)
        // a的scale是1,b的scale是2,所以不相等

        // ✅ 正确方式:使用 compareTo()
        System.out.println("a.compareTo(b): " + a.compareTo(b));  // 0(相等)

        // compareTo()返回值:
        // -1:小于
        //  0:等于
        //  1:大于

        if (a.compareTo(b) == 0) {
            System.out.println("a 等于 b");
        } else if (a.compareTo(b) < 0) {
            System.out.println("a 小于 b");
        } else {
            System.out.println("a 大于 b");
        }
    }
}

总结:

判断相等:用
compareTo(other) == 0
判断大于:用
compareTo(other) > 0
判断小于:用
compareTo(other) < 0

2.5 精度控制


public class BigDecimalScale {
    public static void main(String[] args) {
        BigDecimal num = new BigDecimal("123.456789");

        // 设置精度(保留小数位数)
        BigDecimal scaled1 = num.setScale(2, RoundingMode.HALF_UP);
        System.out.println("保留2位小数:" + scaled1);  // 123.46

        BigDecimal scaled2 = num.setScale(4, RoundingMode.HALF_UP);
        System.out.println("保留4位小数:" + scaled2);  // 123.4568

        // 去除末尾的0
        BigDecimal num2 = new BigDecimal("100.000");
        System.out.println("原始值:" + num2);  // 100.000
        System.out.println("去除末尾0:" + num2.stripTrailingZeros());  // 100

        // 获取精度信息
        System.out.println("scale(小数位数):" + num.scale());  // 6
        System.out.println("precision(总位数):" + num.precision());  // 9
    }
}

第三章:实战案例分析

3.1 案例1:电商订单金额计算

需求: 计算订单总金额,要求精确到分。


public class OrderCalculation {
    public static void main(String[] args) {
        // 商品单价(元)
        BigDecimal price1 = new BigDecimal("99.99");
        BigDecimal price2 = new BigDecimal("158.80");

        // 商品数量
        BigDecimal quantity1 = new BigDecimal("3");
        BigDecimal quantity2 = new BigDecimal("2");

        // 计算小计
        BigDecimal subtotal1 = price1.multiply(quantity1);
        BigDecimal subtotal2 = price2.multiply(quantity2);

        System.out.println("商品1小计:" + subtotal1);
        System.out.println("商品2小计:" + subtotal2);

        // 计算总金额
        BigDecimal total = subtotal1.add(subtotal2);
        System.out.println("订单总金额:" + total);

        // 优惠折扣:9折
        BigDecimal discount = new BigDecimal("0.9");
        BigDecimal finalAmount = total.multiply(discount)
            .setScale(2, RoundingMode.HALF_UP);  // 保留2位小数

        System.out.println("优惠后金额:" + finalAmount);
    }
}

输出:


商品1小计:299.97
商品2小计:317.60
订单总金额:617.57
优惠后金额:555.81

3.2 案例2:银行存款利息计算

需求: 计算定期存款利息,本金10000元,年利率2.75%,存期1年。


public class InterestCalculation {
    public static void main(String[] args) {
        // 本金
        BigDecimal principal = new BigDecimal("10000");

        // 年利率(2.75%)
        BigDecimal annualRate = new BigDecimal("0.0275");

        // 存期(天数)
        int days = 365;

        // 计算利息:本金 × 年利率 × 天数 / 365
        BigDecimal interest = principal
            .multiply(annualRate)
            .multiply(new BigDecimal(days))
            .divide(new BigDecimal("365"), 2, RoundingMode.HALF_UP);

        System.out.println("本金:" + principal + " 元");
        System.out.println("年利率:" + annualRate.multiply(new BigDecimal("100")) + "%");
        System.out.println("存期:" + days + " 天");
        System.out.println("利息:" + interest + " 元");

        // 本息合计
        BigDecimal total = principal.add(interest);
        System.out.println("本息合计:" + total + " 元");
    }
}

输出:


本金:10000 元
年利率:2.75%
存期:365 天
利息:275.00 元
本息合计:10275.00 元

3.3 案例3:税费计算

需求: 计算商品含税价格,税率13%。


public class TaxCalculation {
    public static void main(String[] args) {
        // 不含税价格
        BigDecimal priceExcludingTax = new BigDecimal("1000.00");

        // 税率 13%
        BigDecimal taxRate = new BigDecimal("0.13");

        // 计算税额
        BigDecimal taxAmount = priceExcludingTax
            .multiply(taxRate)
            .setScale(2, RoundingMode.HALF_UP);

        // 含税价格
        BigDecimal priceIncludingTax = priceExcludingTax.add(taxAmount);

        System.out.println("不含税价格:" + priceExcludingTax + " 元");
        System.out.println("税率:" + taxRate.multiply(new BigDecimal("100")) + "%");
        System.out.println("税额:" + taxAmount + " 元");
        System.out.println("含税价格:" + priceIncludingTax + " 元");

        // 反向计算:从含税价格计算不含税价格
        BigDecimal reversePrice = priceIncludingTax
            .divide(BigDecimal.ONE.add(taxRate), 2, RoundingMode.HALF_UP);
        System.out.println("
反向计算不含税价格:" + reversePrice + " 元");
    }
}

输出:


不含税价格:1000.00 元
税率:13%
税额:130.00 元
含税价格:1130.00 元

反向计算不含税价格:1000.00 元

第四章:常见错误与最佳实践

4.1 常见错误总结

错误1:使用double构造

// ❌ 错误
BigDecimal bd = new BigDecimal(0.1);

// ✅ 正确
BigDecimal bd = new BigDecimal("0.1");
错误2:除法不指定精度

// ❌ 错误(会抛异常)
BigDecimal result = a.divide(b);

// ✅ 正确
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);
错误3:使用equals()比较

// ❌ 错误
if (a.equals(b)) { }

// ✅ 正确
if (a.compareTo(b) == 0) { }
错误4:忽略不可变性

BigDecimal amount = new BigDecimal("100");
amount.add(new BigDecimal("50"));  // ❌ 这样没有效果
System.out.println(amount);  // 仍然是100

// ✅ 正确
amount = amount.add(new BigDecimal("50"));
System.out.println(amount);  // 150

4.2 最佳实践

实践1:封装工具类

public class BigDecimalUtil {
    /**
     * 加法
     */
    public static BigDecimal add(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.add(b2);
    }

    /**
     * 减法
     */
    public static BigDecimal subtract(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.subtract(b2);
    }

    /**
     * 乘法
     */
    public static BigDecimal multiply(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.multiply(b2);
    }

    /**
     * 除法
     * @param scale 保留小数位数
     */
    public static BigDecimal divide(double v1, double v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("精度不能小于0");
        }
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.divide(b2, scale, RoundingMode.HALF_UP);
    }

    /**
     * 比较大小
     * @return v1 > v2 返回1, v1 = v2 返回0, v1 < v2 返回-1
     */
    public static int compare(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.compareTo(b2);
    }
}
实践2:金额类封装

public class Money {
    private BigDecimal amount;

    public Money(String amount) {
        this.amount = new BigDecimal(amount);
    }

    public Money add(Money other) {
        return new Money(this.amount.add(other.amount).toString());
    }

    public Money subtract(Money other) {
        return new Money(this.amount.subtract(other.amount).toString());
    }

    public Money multiply(BigDecimal multiplier) {
        return new Money(this.amount.multiply(multiplier).toString());
    }

    public Money divide(BigDecimal divisor, int scale) {
        BigDecimal result = this.amount.divide(divisor, scale, RoundingMode.HALF_UP);
        return new Money(result.toString());
    }

    public boolean isGreaterThan(Money other) {
        return this.amount.compareTo(other.amount) > 0;
    }

    public boolean isLessThan(Money other) {
        return this.amount.compareTo(other.amount) < 0;
    }

    public boolean isEqualTo(Money other) {
        return this.amount.compareTo(other.amount) == 0;
    }

    @Override
    public String toString() {
        return amount.setScale(2, RoundingMode.HALF_UP).toString();
    }
}

4.3 性能优化建议

建议1:复用常量

public class Constants {
    public static final BigDecimal ZERO = BigDecimal.ZERO;
    public static final BigDecimal ONE = BigDecimal.ONE;
    public static final BigDecimal TEN = BigDecimal.TEN;
    public static final BigDecimal HUNDRED = new BigDecimal("100");

    // 常用税率
    public static final BigDecimal TAX_RATE_13 = new BigDecimal("0.13");
    public static final BigDecimal TAX_RATE_6 = new BigDecimal("0.06");
}
建议2:批量计算优化

// ❌ 效率低
BigDecimal sum = BigDecimal.ZERO;
for (BigDecimal num : numbers) {
    sum = sum.add(num);  // 每次创建新对象
}

// ✅ 推荐(如果数据量大)
BigDecimal sum = numbers.stream()
    .reduce(BigDecimal.ZERO, BigDecimal::add);

第五章:高级主题

5.1 BigDecimal与数据库

在数据库中存储金额时的建议:


-- ✅ 推荐方式1:使用DECIMAL类型
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    amount DECIMAL(19, 2)  -- 19位总长度,2位小数
);

-- ✅ 推荐方式2:存储为分(整数)
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    amount_cents BIGINT  -- 以分为单位存储
);

Java代码:


// 方式1:直接存储BigDecimal
public void saveOrder(Order order) {
    String sql = "INSERT INTO orders (id, amount) VALUES (?, ?)";
    PreparedStatement ps = conn.prepareStatement(sql);
    ps.setLong(1, order.getId());
    ps.setBigDecimal(2, order.getAmount());  // 直接设置BigDecimal
    ps.executeUpdate();
}

// 方式2:转为分存储
public void saveOrder(Order order) {
    BigDecimal amountInYuan = order.getAmount();
    long amountInCents = amountInYuan
        .multiply(new BigDecimal("100"))
        .longValue();

    String sql = "INSERT INTO orders (id, amount_cents) VALUES (?, ?)";
    PreparedStatement ps = conn.prepareStatement(sql);
    ps.setLong(1, order.getId());
    ps.setLong(2, amountInCents);
    ps.executeUpdate();
}

5.2 BigDecimal与JSON序列化


// 使用Jackson
public class Order {
    private Long id;

    @JsonSerialize(using = ToStringSerializer.class)  // 序列化为字符串
    private BigDecimal amount;

    // getters and setters
}

// 输出JSON:
// {"id": 1001, "amount": "99.99"}

5.3 BigDecimal的内部实现

了解原理,用得更好:

BigDecimal内部由两部分组成:


BigInteger intVal
:未缩放的整数值
int scale
:小数点后的位数

例如:
123.45
在内部表示为:


intVal = 12345

scale = 2

这就是为什么
equals()
会比较scale的原因:


new BigDecimal("1.0")   // intVal=10, scale=1
new BigDecimal("1.00")  // intVal=100, scale=2
// 虽然值相等,但表示不同!

总结

✅ 必须记住的5条铁律

永远不要用double构造BigDecimal


// ❌ new BigDecimal(0.1)
// ✅ new BigDecimal("0.1")

除法必须指定精度和舍入模式


result = a.divide(b, 2, RoundingMode.HALF_UP);

比较大小用compareTo,不用equals


if (a.compareTo(b) == 0) { }

BigDecimal是不可变的


amount = amount.add(other);  // 必须接收返回值

金融系统必须用BigDecimal


// 涉及金额,一律用BigDecimal

📌 适用场景

✅ 金融系统(银行、支付、理财)✅ 电商系统(订单、价格)✅ 会计系统(账目、报表)✅ 任何需要精确小数计算的场景


有任何疑问,欢迎随时提问!

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...