应优先用 Set 而非 List 存不重复数据,因其自动判重、省去人工校验;HashSet 适合高性能无序场景,TreeSet 支持排序与范围查询,LinkedHashSet 保留插入顺序;判重依赖 equals() 和 hashCode() 一致实现。

为什么用 Set 而不是 List 存不重复数据
当你需要“自动去重”时,List 无法帮你判断是否已存在相同元素——每次 add() 都照单全收,得靠人工遍历 contains() 再决定加不加,既啰嗦又容易漏。而 Set 的契约就是“不接受重复”,底层在插入时直接拦截,省掉显式校验逻辑。
常见误用场景:
- 用 ArrayList 收一堆 ID,再手动去重(比如调 stream().distinct()),其实一开始就该选 Set
- 把 Set 当成“有序容器”用,结果发现 HashSet 迭代顺序不稳定,这是设计使然,不是 bug
HashSet、TreeSet、LinkedHashSet 怎么选
三者都实现 Set,但行为差异直接影响功能和性能:
-
HashSet:基于HashMap实现,增删查平均O(1),但不保证顺序,且要求元素正确重写hashCode()和equals() -
TreeSet:基于红黑树,自动按自然序或自定义Comparator排序,增删查O(log n),适合需要范围查询(如subSet())或天然有序的场景 -
LinkedHashSet:哈希表 + 双向链表,保留插入顺序,性能略低于HashSet(多维护链表开销),但迭代顺序可预测,适合做缓存去重或需按添加顺序遍历的场合
“重复”的判定标准到底是什么
Set 判重只看两件事:equals() 返回 true 且 hashCode() 值相等。缺一不可。
典型翻车点:
- 自定义类没重写 hashCode(),只重写了 equals() → 同样内容的对象可能被当成两个不同元素存进 HashSet
- 重写了 hashCode() 但用了可变字段(比如某个 status 字段参与计算),之后改了字段值 → 对象可能再也找不到了(因为哈希桶位置变了,但集合没重新散列)
- 使用 TreeSet 时,compareTo() 和 equals() 行为不一致(比如一个按 ID 比,一个按 name 比)→ 集合行为混乱,甚至出现“能 add 却 contains 不到”的诡异现象
从 Set 到实际业务逻辑的衔接细节
别只盯着“不重复”这个表层能力,几个关键衔接点常被忽略:
立即学习“Java免费学习笔记(深入)”;
- 转成数组或列表时,用
set.toArray(new T[0]),别用new T[set.size()]—— 后者在某些 JDK 版本下有潜在扩容风险 - 并发场景下,
HashSet不安全,别直接套Collections.synchronizedSet()就完事;高频读写优先考虑ConcurrentSkipListSet或CopyOnWriteArraySet(注意后者写操作成本高) - 如果只是临时去重且后续要频繁查“是否包含某值”,
Set比List+stream().anyMatch()快一个数量级,但若只查一次,创建Set的初始化开销反而可能得不偿失
真正难的不是选哪个实现类,而是想清楚“不重复”背后的真实约束:是值相等即排斥?还是排序后相邻才算重复?抑或需要线程安全下的最终一致性?这些决定了你到底该用什么,以及怎么用。










