为什么不能用 BigDecimal 的 equals 方法做等值比较?
结论:BigDecimal.equals 比较的是数值 + 标度(scale),1.0 与 1.00 数值相等但 equals 返回 false;而 compareTo 只比较数值大小,返回 0 才代表"数值相等"。因此判断金额是否相等一律用 compareTo(...) == 0,或用 signum() 判断是否为零。更隐蔽的坑是:hashCode 也把 scale 算进去,所以把 BigDecimal 当 HashMap/HashSet 的 key 时,1.0 和 1.00 会被当成两个不同的键。
1. 两个字段决定一切
new BigDecimal("1.0") → unscaledValue = 10, scale = 1 (值 = 10 × 10⁻¹)
new BigDecimal("1.00") → unscaledValue = 100, scale = 2 (值 = 100 × 10⁻²)
equals: scale 相等 且 unscaledValue 相等 且 类型相同 → false
compareTo:对齐 scale 后比较数值 → 0
hashCode:同时混入 unscaledValue 与 scale → 不同
BigDecimal 的不可变表示就是"未缩放整数 + 标度",数值相同不代表内部状态相同。equals 用的是内部状态,compareTo 用的是数值语义。
2. 代码:三种比较方式的结果差异
public class BigDecimalEqualsDemo {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.equals(b)); // false:scale 不同
System.out.println(a.compareTo(b)); // 0:数值相同
System.out.println(a.hashCode() == b.hashCode()); // false
Set<BigDecimal> set = new HashSet<>();
set.add(a);
System.out.println(set.contains(b)); // false,因为 hashCode 与 equals 都不等
// 数据库取回 DECIMAL(18,2) 的 0.00,与常量 0 比较
BigDecimal fromDb = new BigDecimal("0.00");
System.out.println(fromDb.equals(BigDecimal.ZERO)); // false
System.out.println(fromDb.compareTo(BigDecimal.ZERO) == 0); // true
System.out.println(fromDb.signum() == 0); // true,最简洁
}
}
signum() 返回 -1、0、1,判断"是否为零"比 compareTo(...) == 0 更直观,也不会受 scale 影响。
3. 各操作的行为对照
| 操作 | 依据 | 1.0 vs 1.00 |
|---|---|---|
equals | 数值 + scale + 类型 | false |
compareTo | 仅数值 | 0 |
hashCode | 含 scale | 不同 |
HashSet.contains | hashCode + equals | false |
TreeMap 的 key | compareTo | 视为同一 key(后放入覆盖前者) |
| 排序 | compareTo | 相等 |
与 double 互转 | doubleValue() 可能丢精度 | 不参与比较逻辑 |
注意 TreeMap 那一行:Comparable 的契约建议"compareTo == 0 应与 equals 一致",BigDecimal 恰好违反了这条建议,官方文档也明确说明了这一点。于是同一个 BigDecimal 放进 HashSet 是两个元素、放进 TreeSet 是一个元素——这种不一致很容易写出难查的 bug。
4. 正确做法
// 1) 判数值相等
if (actual.compareTo(expected) == 0) { /* 相等 */ }
// 2) 判零
if (amount.signum() == 0) { /* 零金额 */ }
// 3) 需要作为 key 时,先统一 scale
BigDecimal key = amount.setScale(2, RoundingMode.HALF_UP);
// 4) 去掉多余的尾零(注意 0 的历史行为,优先 setScale 归一)
BigDecimal normalized = amount.stripTrailingZeros();
setScale(2, ...) 是关键:数据库列是 DECIMAL(18,2),那么内存里也统一成 scale=2 再比较、再入 map,equals 就恢复成可用状态。反过来,如果业务允许"1.0 元和 1.00 元"是不同精度语义(极少),才需要保留 scale 差异。
5. 常见追问
| 追问 | 回答要点 |
|---|---|
为什么 equals 要比较 scale? | BigDecimal 是"未缩放值 + 标度"的精确十进制表示,作者认为精度也是状态的一部分 |
那 compareTo 有没有坑? | 只比数值,null 入参会 NPE;比较两个不同 scale 的大数时会临时扩大数组,开销略高 |
数据库里 0 和 0.00 会有问题吗? | 会,DECIMAL 读回 scale 固定为列定义,若代码里用 new BigDecimal("0") 比较就会漏判 |
能用 == 吗? | 不能,BigDecimal 是对象,== 比引用;BigDecimal.ZERO 是常量但不是唯一实例 |
| 金额为 null 怎么办? | 用 Objects.equals 或先判空;不要让 null 进入比较逻辑 |
| 序列化会不会改变 scale? | 可能,JSON 反序列化常按字面量构造,1.0 与 1.00 会保留不同 scale,跨系统对齐要在边界统一 |
一个实战建议:在 DAO 层出口统一 setScale(2),领域层只处理归一后的值。这样无论数据来自数据库、MQ 还是 HTTP,equals 与 hashCode 的行为都稳定。
小结
equals 比"值 + 精度",compareTo 只比"值"。判等用 compareTo == 0(或 signum() 判零),做 key 前先 setScale 归一。记住 BigDecimal 与 Comparable 契约不一致这件事,面试时往往就是加分点。