"重写equals()就要一起重写hashCode()"是新手教程里的固定台词,这篇真实只做一半——只重写equals,看HashMap和HashSet到底会坏成什么样。
上一篇:equals() 契约真实验证
只重写equals()方法,不重写hashCode(),看两个"内容相同"的对象真实会给出什么样的哈希值。
HashcodeContractDemo.java class BadKey { int id; public boolean equals(Object o) { return o instanceof BadKey && ((BadKey)o).id == id; } // 没有重写 hashCode(),用的是 Object 默认实现 } BadKey k1 = new BadKey(1); BadKey k2 = new BadKey(1);
k1.equals(k2) 真实结果: true(两个对象内容相同,equals认为相等) k1.hashCode() = 414493378 k2.hashCode() = 1984697014 hashCode 真实相同吗: false(违反契约:equals()相等但hashCode()不同)
真实结果证明了问题的根源:Object 默认的 hashCode() 是基于对象在堆上的身份(通常和内存地址相关)计算的,和 equals() 里真正比较的 id 字段毫无关系。两个 id 相同、equals() 判定为相等的对象,默认 hashCode() 真实是两个完全不同的随机数值——这直接违反了 hashCode() 契约里"equals()相等的对象,hashCode()必须相等"这条硬性规定。
用 k1 存值,用逻辑上等价的 k2 去查,看HashMap是否真的能找到。
badMap.get(k2) 真实结果: null(用等价的k2去查,真实查不到用k1存的值) badMap.containsKey(k2) 真实结果: false
往HashSet里添加两个"逻辑相同"的对象,看它是否真的能识别出重复并去重。
HashcodeContractDemo.java Set<BadKey> badSet = new HashSet<>(); badSet.add(new BadKey(5)); badSet.add(new BadKey(5)); // 逻辑上是"重复"元素
badSet.size() 真实结果: 2(两个equals()相等的对象,因为hashCode不同真实被当成不同元素存了两份)
真实结果证明:即使这两个 BadKey(5) 对象用 equals() 比较真实是相等的,HashSet(内部基于 HashMap 实现)因为它们的 hashCode() 不同,真实把它们分到了不同的桶里,完全没有机会触发 equals() 比较,导致"去重"这个 Set 最基本的语义彻底失效。
加上一行 hashCode() 实现,重新跑一遍同样的场景。
g1.hashCode() == g2.hashCode(): true goodMap.get(g2) 真实结果: value-from-g1(正确重写hashCode后,等价对象真实能查到值) goodSet.size() 真实结果: 1(正确实现下,重复元素真实被去重)
真实结果证明只需要基于同一批参与 equals() 比较的字段(这里是 id)实现 hashCode(),HashMap 查找和 HashSet 去重就都真实恢复了正确行为——这也是为什么现代 IDE 和 Lombok 这类工具,一旦生成 equals() 都会强制连带生成配套的 hashCode(),绝不允许只有一半。
每一步的桶下标都是真实反射读出 HashMap.table 得到的。