HashSet去重严格按“先hashCode()后equals()”两步执行:桶空则只调hashCode(),桶非空才依次调equals();仅重写equals()不重写hashCode()会导致去重失效,因默认哈希值基于内存地址,相同内容对象被分至不同桶,equals()永不触发。

存入HashSet时,先调用hashCode(),再可能调用equals()
HashSet判断一个新元素是否已存在,严格按两步走:先算hashCode(),再决定要不要调用equals()。不是“两个都调”,也不是“随机选一个”,而是有明确的短路逻辑。
具体流程是:hashCode()返回的值决定了该对象落在哈希表哪个“桶”(bucket)里;如果那个桶里当前为空,直接插入,equals()根本不会被触发;只有桶里已有至少一个对象,才会拿新对象和桶内每个对象依次调用equals()比对。
- 桶为空 → 只调
hashCode(),不调equals() - 桶非空 → 先调
hashCode()定位桶,再对桶内每个对象调equals() -
hashCode()不同 → 绝对不调equals(),哪怕两个对象内容完全一样
为什么只重写equals()却无法去重
因为hashCode()没重写时,默认继承自Object,返回的是内存地址的哈希值。两个内容相同的自定义对象,只要不是同一个实例(==为false),hashCode()就大概率不同 —— 这导致它们被分到不同桶里,equals()压根没机会运行。
常见错误现象:new Person("Alice", 30)加了两次进HashSet,结果集合大小是2,而不是1。打印调试会发现equals()一次都没被调用过。
- 重写
equals()但不重写hashCode()→ 去重失效,且无任何报错或警告 - IDE(如IntelliJ)在重写
equals()时通常会提示“also overridehashCode()”,这不是建议,是契约 - 用
Objects.hash(...)生成hashCode()最安全,它自动处理null和浮点数比较等边界
hashCode()相同但equals()返回false是正常现象
哈希冲突不可避免。比如两个Person对象,一个叫"Bob"年龄25,另一个叫"Tom"年龄25,若hashCode()只基于age计算,它们哈希值就相同,但equals()会因name不同而返回false。这时HashSet会把它们都存进同一个桶,用链表或红黑树组织。
这种设计是权衡:用空间换时间。哈希码负责快速过滤掉99%的不等对象;equals()兜底处理剩下那1%的冲突。
- 哈希值相同 ≠ 对象相等,只是“需要进一步确认”的信号
- 过度追求
hashCode()分散(比如加入随机因子)反而破坏一致性,导致contains()查不到自己刚add()的对象 - 一旦
hashCode()依赖可变字段(如name后续被setName()修改),对象存进HashSet后就再也找不到了
自定义类放进HashSet前必须检查的三件事
不是“写了equals和hashCode就行”,得看它们是否真正协同工作。最容易忽略的是语义一致性 —— 什么情况下你认为两个对象“应该算重复”?这个业务逻辑必须同时体现在两个方法里。
- 确认
equals()中用的字段,和hashCode()里Objects.hash(...)传的字段完全一致 - 验证交换顺序是否影响结果:比如
Comparison(se1, se2)和Comparison(se2, se1)应视为相同,则hashCode()不能简单写成Objects.hash(se1, se2),得用对称写法(如排序后哈希) - 字段含
double类型时,避免直接用==比对,在equals()里用Double.compare(),在hashCode()里用Double.hashCode()而非(int) value
实际开发中,最常踩的坑不是不会写,而是改了业务逻辑后忘了同步更新其中一个方法。只要hashCode()和equals()对同一组字段做同构运算,且不引入可变状态,HashSet的去重行为就是可预测的。










