HashSet通过hashCode()定位桶位置、equals()精判重复,协同实现O(1)平均去重;必须同时重写二者以保证自定义对象去重正确,且add()已封装完整逻辑,无需手动contains校验。

HashSet 通过哈希表结构 + Set 接口契约,天然支持 O(1) 平均时间复杂度的去重。它不依赖“手动比对”,而是靠 hashCode() 和 equals() 协同完成唯一性判断——这是高效去重的核心机制。
利用 Set 接口的“不重复”契约自动拦截重复元素
Set 接口明确要求:集合中不允许存在两个相等(equals() 返回 true)的元素。HashSet 作为实现类,严格遵守这一约定:
- 每次调用 add(e) 时,先计算 e.hashCode() 定位桶位置;
- 若桶中已有元素,再逐个调用 equals() 判断是否真正重复;
- 仅当两者都未匹配时才插入,否则直接返回 false,不新增也不报错。
这意味着你无需写 if 判断或循环查重——add() 本身已封装完整逻辑:
- ✅ 正确用法:`set.add(item)` —— 重复则静默忽略,返回 false
- ❌ 冗余操作:`if (!set.contains(item)) set.add(item)` —— 多一次 hash 查找,性能反而下降
确保自定义对象去重有效的关键:重写 hashCode() 和 equals()
对于 String、Integer 等 JDK 类型,JVM 已提供合理实现;但自定义类(如 User、Order)必须主动重写这两个方法,否则默认使用 Object 的实现(基于内存地址),导致逻辑错误:
- 两个内容相同的对象可能因 hash 值不同被分到不同桶 → 被当成不同元素存入
- 或 hash 相同但
equals()返回 false → 仍被当作不同元素
正确做法(以 User 为例):
立即学习“Java免费学习笔记(深入)”;
@Override
public int hashCode() {
return Objects.hash(id, name); // 用参与 equals 判断的字段构造 hash
}
<p>@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id && Objects.equals(name, user.name);
}注意 null 值和线程安全的边界情况
HashSet 允许且仅允许一个 null 元素(其 hash 值被固定为 0),这符合 Set 接口规范,但需留意:
- 若业务中
null是有效数据,无需特殊处理;若应禁止,添加前自行判空 - HashSet 不是线程安全的。多线程并发
add()可能引发数据错乱或异常,此时应选Collections.synchronizedSet(new HashSet())或ConcurrentHashMap.newKeySet()
对比其他去重方式,突显 HashSet 的优势
相比常见替代方案,HashSet 在通用去重场景下效率更优:
- ArrayList + contains():每次 add 都要遍历 O(n),整体 O(n²)
- TreeSet:基于红黑树,O(log n) 插入,适合需要排序的场景,但纯去重不如 HashSet 快
- Stream.distinct():底层仍用 LinkedHashSet,方便链式调用,但本质一致
只要不强求顺序或线程安全,HashSet 就是最轻量、最直接的去重选择。


















