Java HashSet存储自定义类去重失效原因及解决方法
问题产生的核心原因
HashSet的底层是基于哈希表实现的,它判定元素重复的逻辑从来都不是单独调用equals()方法,而是遵循两步校验规则:
- 先调用元素的
hashCode()方法获取哈希值,根据哈希值找到元素对应的存储桶位置,如果这个位置上没有存过其他元素,直接判定为不重复,直接存入,不会走后续equals校验 - 只有当两个元素的
hashCode()返回值完全相同时,才会进一步调用equals()方法比对,equals()返回true才最终判定为重复元素,拒绝插入。
你当前的User类只重写了equals()方法,没有重写hashCode()方法,会默认调用Object类的原生hashCode()实现——这个原生实现是根据对象在内存中的存储地址计算哈希值的。user1和user2是两次new出来的独立对象,内存地址不同,计算出的哈希值自然不同,HashSet会直接把它们判定为不重复元素分别存入,根本不会触发你写的equals()比对逻辑,最终集合大小就是3,不会达到预期的去重效果。
常见疑问解答
- 存自定义类到Set必须重写
equals()方法吗?
没有统一的强制要求,要根据你用的Set具体实现类来定:- 用
HashSet/LinkedHashSet这类哈希表实现的Set时,必须同时重写符合逻辑一致性的equals()和hashCode()方法:如果两个对象通过equals()判定为相等,它们的hashCode()返回值必须完全相同,否则去重逻辑一定失效。 - 用
TreeSet这类基于红黑树、按排序规则存储的Set时,不需要重写equals()和hashCode(),但要保证元素实现Comparable接口的compareTo()方法,或者在构造TreeSet时传入自定义比较器,两个元素比较返回0时就会被判定为重复。
- 用
- Set不是自带自动去重能力吗?为什么会失效?
Set只是接口规范,所谓“自动去重”是各个实现类按照自己的判定规则完成的,没有脱离实现逻辑的“魔法式去重”:不满足对应实现类的重复判定条件,自然不会触发去重。你当前的代码只适配了equals()的判断逻辑,完全没适配HashSet依赖的hashCode()校验规则,去重失效是必然结果。
修复方案
首先给User类补充重写hashCode()方法,逻辑和equals()保持一致,基于你判定重复的核心字段uid生成哈希值即可:
@Override public int hashCode() { return uid == null ? 0 : uid.hashCode(); }
另外你当前写的equals()方法存在健壮性问题:没有判断入参是否为空、是否是User类型,遇到非法入参会直接抛出类型转换异常,可以优化为更稳妥的写法:
@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; User targetUser = (User) obj; return uid != null ? uid.equals(targetUser.uid) : targetUser.uid == null; }
修改完成后重新运行测试代码,user1和user2会因为uid相同、哈希值一致、equals比对返回true被HashSet判定为重复元素,最终集合大小为2,符合预期。
内容的提问来源于stack exchange,提问作者Elly
相关产品推荐
相关产品推荐

