BigDecimal 和 Long 哪个表示金额更合适,怎么选择?
结论:两者都不错,选哪个取决于运算形态。只要加减和简单比较,用 long 存最小币种单位(分、厘)最好:基本类型、无对象开销、精确、能做原子累加。涉及乘法、除法、汇率换算、折扣分摊、按比例拆分等会产生小数的场景,用 BigDecimal:任意精度十进制,配合 scale 与 RoundingMode 才能表达"保留两位、四舍五入"。没有统一答案,但同一系统内必须统一单位与舍入规则,混用才是事故来源。
1. 两种方案的对比
| 维度 | long(分) | BigDecimal |
|---|---|---|
| 精度 | 整数精确到分/厘 | 任意精度十进制,需显式 scale |
| 内存与性能 | 8 字节,栈上运算,最快 | 对象 + BigInteger,慢一个数量级 |
| 除法 | 需自己处理余数(分摊) | divide(divisor, scale, RoundingMode) |
| 表示范围 | 约 ±9.2×10¹⁸ 分 ≈ ±9.2×10¹⁶ 元 | 只受内存限制 |
| 数据库字段 | BIGINT | DECIMAL(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(); }
}
三个设计要点:
- 构造入口收窄:只暴露
ofYuan(String),杜绝new Money(19.99)这种隐式精度丢失; Math.addExact:金额溢出必须显式失败,绝不能静默回绕成负数;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、统一舍入模式,比选哪个类型更重要。