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<K,Object> 无序 允许 最快(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 生产避坑总结

  1. 永远不要在 for-each 循环中直接 remove(除非用 Iterator.remove)
  2. ArrayList 作为方法返回类型时,考虑 Collections.unmodifiableList / List.copyOf
  3. 频繁头尾操作 → 优先 LinkedList / ArrayDeque 而非 ArrayList
  4. TreeSet / TreeMap 的 Comparator 要保持一致性(equals 与 compareTo 语义一致)
  5. 大集合去重优先 Stream.distinct() 或 LinkedHashSet 构造,而不是反复 contains
  6. 并发场景优先 ConcurrentSkipListSet / CopyOnWrite / ConcurrentHashMap.keySet()
  7. 内存敏感场景:小对象集合用 Trove / fastutil / Eclipse Collections 替代

你当前项目里 List/Set 用得最多的痛点是什么?

  • 大 List 频繁中间插入/删除?
  • 高并发去重?
  • 排序 + 范围查询?
  • Stream 去重顺序不对?
  • 内存占用爆炸?

告诉我具体场景,我可以继续给针对性代码 + 替换方案。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐