为什么不能用 BigDecimal 的 equals 方法做等值比较?

#Java#面试八股

为什么不能用 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.containshashCode + equalsfalse
TreeMap 的 keycompareTo视为同一 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 契约不一致这件事,面试时往往就是加分点。

笔记加载中…