Set接口核心特点是元素唯一且无索引,依赖hashCode()和equals()联合判定重复;add()返回false即表示元素已存在,是Set最直接的重复反馈机制。

Set 接口的核心特点就是「元素唯一 + 无索引」,不是“随便不重复”,而是靠 hashCode() 和 equals() 联合判定是否重复。 直接用 HashSet 去重时出错,八成是没重写这两个方法,或误以为“内容一样就自动去重”。
为什么 add() 返回 false 就代表重复?
这是 Set 最直接的反馈机制:add() 方法返回 boolean,true 表示新增成功,false 表示该元素已存在、被拒绝。它不是抛异常,也不是静默吞掉——你必须检查返回值才能确认是否真的加进去了。
- 对基本类型和
String等 JDK 类型,hashCode()和equals()已实现好,直接用没问题 - 对自定义类(如
Student、User),若不重写hashCode()和equals(),哪怕两个对象字段完全相同,HashSet也认为是不同元素(因为默认用内存地址判断) - 只重写其中一个方法也不行:哈希值不同 → 直接分到不同桶里,
equals()根本不会被调用;哈希值相同但equals()没重写 → 默认比较地址,还是不等
HashSet 去重到底怎么走流程?
不是“看一眼内容就拉黑”,而是一套两级校验:
- 第一步:计算
e.hashCode(),定位哈希桶(数组下标) - 第二步:如果桶为空 → 直接存入;如果桶非空 → 遍历桶内所有元素,对每个元素
old执行e.hashCode() == old.hashCode() && e.equals(old) - 只有两者都为
true,才认定重复,add()返回false
所以,hashCode() 是“快速分流”,equals() 是“最终拍板”。缺一不可。
立即学习“Java免费学习笔记(深入)”;
常见踩坑场景与对应解法
这些不是理论问题,是上线后真会报错的点:
-
场景:用
TreeSet存null或未实现Comparable的对象 → 抛NullPointerException或ClassCastException
解法:要么改用HashSet,要么确保类实现Comparable,或显式传入Comparator -
场景:List 转 Set 后顺序乱了,以为“去重失败”
解法:这不是 bug,是HashSet的设计特性;需要保持插入顺序就换LinkedHashSet -
场景:多线程往同一个
HashSet里add(),结果 size 不稳定或抛ConcurrentModificationException
解法:HashSet本身线程不安全;高并发下要么用Collections.synchronizedSet(),要么用ConcurrentHashMap.newKeySet()(Java 8+)
什么时候该选哪个 Set 实现?
别只盯着“去重”,得看后续怎么用:
- 只要去重 + 快速查存(
contains()、add())→ 选HashSet(O(1)) - 去重 + 按插入顺序遍历(比如日志去重后要按时间展示)→ 选
LinkedHashSet(O(1),略慢于HashSet) - 去重 + 要排序输出(比如用户积分榜)→ 选
TreeSet(O(log n),且不能存null)
最常被忽略的一点:如果你后续要频繁调用 contains() 判断是否存在,HashSet 的 O(1) 对比 List.contains() 的 O(n) 是数量级差异——哪怕只有几百条数据,实测也能快 10 倍以上。










