BigDecimal 和 Long 哪个表示金额更合适,怎么选择?

#Java#面试八股

BigDecimal 和 Long 哪个表示金额更合适,怎么选择?

结论:两者都不错,选哪个取决于运算形态。只要加减和简单比较,用 long 存最小币种单位(分、厘)最好:基本类型、无对象开销、精确、能做原子累加。涉及乘法、除法、汇率换算、折扣分摊、按比例拆分等会产生小数的场景,用 BigDecimal:任意精度十进制,配合 scale 与 RoundingMode 才能表达"保留两位、四舍五入"。没有统一答案,但同一系统内必须统一单位与舍入规则,混用才是事故来源。

1. 两种方案的对比

维度long(分)BigDecimal
精度整数精确到分/厘任意精度十进制,需显式 scale
内存与性能8 字节,栈上运算,最快对象 + BigInteger,慢一个数量级
除法需自己处理余数(分摊)divide(divisor, scale, RoundingMode)
表示范围约 ±9.2×10¹⁸ 分 ≈ ±9.2×10¹⁶ 元只受内存限制
数据库字段BIGINTDECIMAL(18,2)
序列化JSON 数字(注意 JS 精度)建议序列化成字符串
主要坑元/分单位混用、溢出equals/scale、除法不指定精度抛异常

经验法则:领域模型内部用 long 分,对外接口和展示用 BigDecimal;跨币种、税率、汇率这类天然带小数的计算留在 BigDecimal 域内完成,最后落库或展示时再转。

2. 代码:一个以分为单位的金额类

public final class Money {
    private final long cents;                 // 唯一状态,单位:分

    private Money(long cents) { this.cents = cents; }

    public static Money ofYuan(String yuan) { // 入参用字符串,避免 double
        return new Money(new BigDecimal(yuan).movePointRight(2).longValueExact());
    }

    public Money plus(Money other) {
        return new Money(Math.addExact(cents, other.cents)); // 溢出直接抛异常
    }

    public Money times(BigDecimal rate) {     // 乘汇率必须舍入
        BigDecimal v = BigDecimal.valueOf(cents).multiply(rate)
                .setScale(0, RoundingMode.HALF_UP);
        return new Money(v.longValueExact());
    }

    public BigDecimal toYuan() { return BigDecimal.valueOf(cents, 2); }

    @Override public String toString() { return toYuan().toPlainString(); }
}

三个设计要点:

  1. 构造入口收窄:只暴露 ofYuan(String),杜绝 new Money(19.99) 这种隐式精度丢失;
  2. Math.addExact:金额溢出必须显式失败,绝不能静默回绕成负数;
  3. BigDecimal.valueOf(cents, 2):用 scale=2 直接构造,不要再 divide(new BigDecimal(100))。

3. 除法与分摊:long 方案真正的难点

100 元分给 3 个人,每人 33.33 元还剩 0.01 元。用 BigDecimal 直接除会得到无限小数或丢余数,用 long 则必须自己做最大余额法:

static long[] split(long totalCents, int n) {
    long base = totalCents / n;
    long remainder = totalCents % n;          // 余数按分发给前 remainder 个
    long[] parts = new long[n];
    for (int i = 0; i < n; i++) {
        parts[i] = base + (i < remainder ? 1 : 0);
    }
    return parts;                              // 保证 Σparts == totalCents
}

这类"必须严格对平"的场景(优惠券分摊、退款拆分、对账)恰恰是 long 的强项:整数除法天然可验证总和,而 BigDecimal 若在每一步 HALF_UP,累加后可能多出或少掉一分。

场景推荐理由
账户余额、流水金额long 分加减为主,要快、要对平
订单优惠分摊long 分 + 最大余额法必须总和相等
汇率换算、含税计算BigDecimal中间结果有小数,需指定舍入
报表统计、均值BigDecimal结果允许小数,展示需固定位数
高并发累加(如计数金额)long + LongAdder无锁累加,BigDecimal 不可变不适合

4. 常见追问

追问回答要点
为什么不用 double?IEEE 754 二进制浮点无法精确表示 0.1,误差会累积(见浮点数专题)
BigDecimal 怎么构造?用 String 或 BigDecimal.valueOf,禁止 new BigDecimal(double)
除法为什么报错?结果是无限小数时必须给 scale 和 RoundingMode,否则抛 ArithmeticException
RoundingMode 选哪个?金融常用 HALF_UP(四舍五入);HALF_EVEN(银行家舍入)适合统计与多次累加
BigDecimal 可以当 Map 的 key 吗?可以但危险,equals 含 scale,1.0 与 1.00 不同 key
落库用 BIGINT 还是 DECIMAL?与代码单位一致:存分用 BIGINT,存元用 DECIMAL(18,2),别一边分一边元

再多说一句溢出:long 分能表示约 9.2 亿亿元,业务上足够;但乘法中间结果(金额 × 数量 × 汇率)可能先溢出,所以 BigDecimal 更适合做中间计算,long 只做最终存储。

小结

选型不是"哪个更高级",而是"谁承担小数"。加减、对账、并发累加用 long 分;乘除、汇率、分摊用 BigDecimal 并显式舍入。落地时统一单位、统一 scale、统一舍入模式,比选哪个类型更重要。

笔记加载中…