Java 集合框架进阶:List 与 Set 的深度解析与实战(2025–2026 视角)
List 和 Set 是日常开发中使用频率最高的两种集合接口,但它们在底层实现、性能特征、使用场景、线程安全、内存占用、遍历方式等方面差异非常大。真正把它们搞明白,能帮你避免 80% 的集合相关 bug 和性能坑。
一、核心区别对比表(面试 + 生产必背)
| 维度 | List | Set | 备注 / 常见误区 |
|---|---|---|---|
| 是否允许重复元素 | 允许 | 不允许(equals 判定) | Set 靠 equals + hashCode 去重 |
| 是否有顺序 | 有序(插入顺序 / 索引顺序) | 大部分无序(HashSet / LinkedHashSet 有序) | TreeSet 按 Comparable / Comparator 排序 |
| 是否支持索引访问 | 支持(get/set/remove by index) | 不支持(没有 get(int)) | 这是最本质的区别 |
| 允许 null 元素 | 允许(ArrayList / LinkedList 都允许) | 大部分允许 1 个 null(除 TreeSet) | TreeSet / ConcurrentSkipListSet 不允许 null |
| 典型实现类 | ArrayList、LinkedList、Vector、CopyOnWriteArrayList | HashSet、LinkedHashSet、TreeSet、EnumSet、ConcurrentSkipListSet | — |
| 线程安全实现 | Collections.synchronizedList / CopyOnWriteArrayList | Collections.synchronizedSet / ConcurrentSkipListSet / CopyOnWriteArraySet | ConcurrentHashMap.newKeySet() 也很常用 |
| 内存占用排序 | LinkedList > ArrayList > Vector | HashSet ≈ LinkedHashSet > TreeSet | LinkedList 每个节点有 prev/next 指针 |
| 随机访问性能 | ArrayList O(1) / LinkedList O(n) | 无索引访问 | — |
| 插入/删除性能 | ArrayList 中间插入 O(n) LinkedList 任意位置 O(1)(已知位置) | HashSet / LinkedHashSet O(1) 均摊 TreeSet O(log n) | — |
| 遍历性能 | ArrayList > LinkedList(缓存局部性) | HashSet ≈ LinkedHashSet > TreeSet | LinkedList 遍历最慢(指针跳跃) |
二、三大主流 List 实现深度对比
| 实现类 | 底层数据结构 | 随机访问 | 插入/删除(头部) | 插入/删除(尾部) | 插入/删除(中间) | 内存开销 | 典型使用场景 |
|---|---|---|---|---|---|---|---|
| ArrayList | 动态数组(Object[]) | O(1) | O(n) | O(1) 均摊 | O(n) | 低 | 99% 场景首选、作为默认 List |
| LinkedList | 双向链表 + deque | O(n) | O(1) | O(1) | O(n)(需遍历) | 高 | 频繁头尾操作、队列/栈、Deque |
| Vector | 动态数组 + synchronized | O(1) | O(n) | O(1) 均摊 | O(n) | 低 | 极少用(已过时) |
| CopyOnWriteArrayList | 数组 + 写时复制 | O(1) | O(n) 写慢 | O(n) 写慢 | O(n) 写慢 | 高 | 读多写极少、事件监听器列表 |
ArrayList 容量增长策略(JDK 8–21 通用):
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
// 如果还不够,则至少扩到 minCapacity
生产建议:提前知道大致容量 → new ArrayList<>(10000) 避免多次扩容拷贝。
三、Set 的四大主流实现深度对比
| 实现类 | 底层结构 | 顺序性 | null 支持 | 线程安全原生支持 | 性能排序(增/查/删) | 典型使用场景 |
|---|---|---|---|---|---|---|
| HashSet | HashMap | 无序 | 允许 | 否 | 最快(O(1) 均摊) | 普通去重、快速判断存在 |
| LinkedHashSet | HashMap + 双向链表 | 插入顺序 | 允许 | 否 | 略慢于 HashSet | 需要保持插入顺序的去重集合 |
| TreeSet | 红黑树(TreeMap) | 自然顺序 / Comparator | 不允许 | 否 | O(log n) | 需要排序、范围查询、first/last/ceiling |
| ConcurrentSkipListSet | 跳表(ConcurrentSkipListMap) | 自然顺序 / Comparator | 不允许 | 是 | O(log n) 并发安全 | 高并发、有序去重 |
| CopyOnWriteArraySet | CopyOnWriteArrayList 包装 | 插入顺序 | 允许 | 是(写时复制) | 读快写极慢 | 读多写极少的并发场景 |
TreeSet vs HashSet 选择口诀:
- 要快、不要顺序 → HashSet
- 要插入顺序 → LinkedHashSet
- 要排序 / 范围查询 / ceiling / floor → TreeSet
- 要并发 + 有序 → ConcurrentSkipListSet
- 要并发 + 插入顺序 + 写很少 → CopyOnWriteArraySet
四、真实业务场景代码模板(直接复制改)
1. 去重 + 保持插入顺序(最常见需求)
// 推荐写法(Java 21+ 更简洁)
Set<String> uniqueOrdered = new LinkedHashSet<>(List.of("a", "b", "a", "c", "b"));
2. Stream 去重(保留首次出现顺序)
List<String> deduped = list.stream()
.distinct() // 内部用 LinkedHashSet 实现,保留顺序
.toList();
3. TreeSet 范围查询(业务统计常用)
TreeSet<Integer> scores = new TreeSet<>();
// ... 添加分数
// 找出 80~100 分的人数
int count = scores.subSet(80, true, 101, false).size();
// 最高分前三
scores.descendingSet().stream().limit(3).forEach(System.out::println);
4. 高并发去重计数(ConcurrentHashMap 技巧)
// 替代 ConcurrentSkipListSet + 计数
ConcurrentHashMap<String, Boolean> seen = new ConcurrentHashMap<>();
seen.putIfAbsent(key, Boolean.TRUE); // 返回 null 表示首次出现
5. CopyOnWriteArrayList/Set 典型用法(配置变更监听)
private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
public void addListener(Listener l) {
listeners.add(l);
}
public void fireEvent(Event e) {
for (Listener l : listeners) { // 遍历期间可安全修改
l.onEvent(e);
}
}
五、2025–2026 生产避坑总结
- 永远不要在 for-each 循环中直接 remove(除非用 Iterator.remove)
- ArrayList 作为方法返回类型时,考虑 Collections.unmodifiableList / List.copyOf
- 频繁头尾操作 → 优先 LinkedList / ArrayDeque 而非 ArrayList
- TreeSet / TreeMap 的 Comparator 要保持一致性(equals 与 compareTo 语义一致)
- 大集合去重优先 Stream.distinct() 或 LinkedHashSet 构造,而不是反复 contains
- 并发场景优先 ConcurrentSkipListSet / CopyOnWrite / ConcurrentHashMap.keySet()
- 内存敏感场景:小对象集合用 Trove / fastutil / Eclipse Collections 替代
你当前项目里 List/Set 用得最多的痛点是什么?
- 大 List 频繁中间插入/删除?
- 高并发去重?
- 排序 + 范围查询?
- Stream 去重顺序不对?
- 内存占用爆炸?
告诉我具体场景,我可以继续给针对性代码 + 替换方案。