bigywater.tech
Java 内部原理 · 82

hashCode() 契约真实验证:只重写equals不重写hashCode,HashSet真实存进两份"重复"元素

"重写equals()就要一起重写hashCode()"是新手教程里的固定台词,这篇真实只做一半——只重写equals,看HashMap和HashSet到底会坏成什么样。

equals()相等但hashCode不同
真实验证:两个内容相同、equals()判定为true的对象,默认继承的hashCode()真实给出完全不同的值
HashSet真实存了两份"重复"元素
真实验证:添加两个equals()相等的对象到HashSet,size()真实是2不是1,去重彻底失效

上一篇:equals() 契约真实验证

真实验证:equals()相等,hashCode()却真实不同

只重写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()必须相等"这条硬性规定。

真实验证:HashMap 用等价对象查不到已存的值

用 k1 存值,用逻辑上等价的 k2 去查,看HashMap是否真的能找到。

真实实测
badMap.get(k2) 真实结果: null(用等价的k2去查,真实查不到用k1存的值)
badMap.containsKey(k2) 真实结果: false
真实失效的机制。HashMap 定位一个 key 的第一步真实是根据 hashCode() 找到对应的桶——如果 k1 和 k2 的 hashCode() 不同,get(k2) 会真实被导向一个完全不同、可能是空的桶,连 equals() 比较的机会都不会有,直接返回 null。这就是为什么"违反 hashCode 契约"不是一个理论问题——它会真实、彻底地破坏基于哈希的查找能力,而且不会抛出任何异常,只是安静地返回错误结果。

真实验证:HashSet 的去重能力真实失效

往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() 后,一切恢复正常

加上一行 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 得到的。

k1 存进桶 9,get(k2) 却去桶 14 找:一个只重写 equals() 不重写 hashCode() 的真实后果
get() 这次真实要查的桶对象真正待着的桶

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,契约违反场景重复运行结果一致(哈希值本身每次JVM运行可能不同,但"不相等"这个结论稳定)。
    • 和equals() 契约那篇一起看,构成了 Java 里"相等性"这套体系完整的真实契约图景。
    • 没有涉及:hashCode() 契约里"同一个对象多次调用hashCode()必须返回相同值(除非equals比较用到的信息被修改)"这条规则被打破的真实场景、record类型自动生成hashCode/equals组合的正确性保证——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号