集合体系总览

如果变量是”装一个数据的盒子”,那么集合就是”装一堆数据的容器”。一个变量只能记一个电话号,而你通讯录里的几百个联系人怎么办?数组虽然能装多个,但长度固定、增删不便——你想在中间插一个人,得把后面所有人往后挪一位。Java 集合框架(Collections Framework)就是为这种”管理一组对象”的需求而生:它提供了一整套精心设计的容器,各有所长,覆盖了几乎所有的数据组织场景。

这一章是第四阶段的开篇。我们先从高空俯瞰整个集合框架的全貌——两大族谱、迭代器机制、选型指南——再在后续章节深入每一个具体容器。

一、为什么需要集合框架

1.1 数组的局限

数组是 Java 最原始的”容器”,但它有明显的短板:

String[] names = new String[3];
names[0] = "Alice";
names[1] = "Bob";
// 想再加一个 Charlie?长度是 3,装不下了
// 只能新建一个更长的数组,把旧的拷过去
String[] bigger = new String[6];
System.arraycopy(names, 0, bigger, 0, names.length);

数组的痛点:

  1. 长度固定——一旦创建,容量不能变。增删元素要手动搬家。
  2. API 贫乏——只有 length 和下标访问,没有 containsindexOfsort 这些便捷方法。
  3. 只能存同类型——虽然这有时是优点(类型安全),但缺乏灵活性。
  4. 无法表达”键值对”——数组是线性的,而很多场景需要”按名字查东西”。

1.2 集合框架的诞生

Java 1.2 引入了集合框架(Collections Framework),它统一了”容器”的抽象:

  • 一套接口CollectionListSetMapQueue 等,定义容器的契约。
  • 多套实现:每个接口有多种实现,按场景选择。
  • 一套算法Collections 工具类提供排序、查找、洗牌等通用算法。

这套设计的美妙之处在于:接口与实现分离。你的代码面向 List 编程,今天用 ArrayList,明天换成 LinkedList,业务逻辑一行都不用改。

二、两大族谱:Collection 与 Map

整个集合框架的根,分两条线:Collection 族(装一组单独的元素)和 Map 族(装键值对映射)。

2.1 Collection 族谱

Collection<E> 是所有”单元素容器”的根接口,它又派生出三大子接口:

Collection
├── List(列表:有序、可重复、可索引)
│   ├── ArrayList      (动态数组)
│   ├── LinkedList     (双向链表)
│   └── CopyOnWriteArrayList (写时复制,并发安全)
├── Set(集合:无序、不可重复)
│   ├── HashSet        (基于 HashMap)
│   ├── LinkedHashSet  (保持插入顺序)
│   └── TreeSet        (基于红黑树,有序)
└── Queue(队列:FIFO 为主)
    ├── PriorityQueue  (优先队列,最小堆)
    ├── ArrayDeque     (双端队列,循环数组)
    └── 各种 BlockingQueue (阻塞队列,并发用)

三大分支的性格:

  • List(列表)——像个排队:有先后顺序,每个人有编号(索引),可以重复。适合”按位置访问”。
  • Set(集合)——像个俱乐部:没有顺序概念(或者说顺序不重要),每个人唯一,不能重复。适合”去重”和”判断存在性”。
  • Queue(队列)——像个取号窗口:讲究进出的规则(先进先出、优先级、栈式后进先出)。适合”按特定顺序处理元素”。

2.2 Map 族谱

Map<K, V> 是独立的一支——它不继承 Collection,因为它装的不是”单个元素”,而是”键值对”(Entry):

Map
├── HashMap            (数组 + 链表 + 红黑树)
├── LinkedHashMap      (HashMap + 链表,保持顺序)
├── TreeMap            (红黑树,按键排序)
├── Hashtable          (古董,并发安全但已过时)
└── ConcurrentHashMap  (现代并发 Map)

Map 的核心思想是”用键查值”——给一个 key,秒回 value。它就像一本字典:你知道”苹果”这个词(key),就能查到”apple”这个翻译(value)。

💡 为什么 Map 不继承 Collection? 因为 Collection 操作的是单个元素(add(E)remove(Object)),而 Map 操作的是键值对。让 Map 继承 Collection 会让契约变得别扭——add 该加什么?键?值?还是 Entry?Java 选择让 Map 独立,更清晰。不过 Map 仍可通过 entrySet()keySet()values() 拿到 Collection 视图。

三、Collection 接口的核心方法

Collection 接口定义了所有容器共享的基本操作:

public interface Collection<E> extends Iterable<E> {
    // 基本操作
    int size();                    // 元素个数
    boolean isEmpty();             // 是否为空
    boolean contains(Object o);    // 是否包含
    boolean add(E e);              // 添加(Set 会拒绝重复)
    boolean remove(Object o);      // 删除
    void clear();                  // 清空

    // 批量操作
    boolean addAll(Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    boolean retainAll(Collection<?> c);   // 交集
    boolean containsAll(Collection<?> c);

    // 转换
    Object[] toArray();
    <T> T[] toArray(T[] a);

    // 视图
    Iterator<E> iterator();
}

注意 Collection 继承了 Iterable<E>——这正是”可遍历”的契约,下一节详述。

四、Iterable 与 Iterator

4.1 迭代器模式

集合是”容器”,容器里的元素怎么”挨个拿出来”?最朴素的方式是下标(list.get(i)),但 Set 没有下标,Map 更是键值对。Java 用迭代器模式(Iterator Pattern)统一了”遍历”这件事:

  • Iterable<E> 表示”我可被迭代”——它的 iterator() 方法返回一个 Iterator<E>
  • Iterator<E> 是真正的”游标”,提供 hasNext()next()remove()
public interface Iterable<E> {
    Iterator<E> iterator();
    // Java 8+ 还有 forEach 和 spliterator
}

public interface Iterator<E> {
    boolean hasNext();   // 还有下一个吗?
    E next();            // 取出下一个,游标前移
    default void remove() { /* 可选 */ }
}

4.2 手动迭代

List<String> list = List.of("A", "B", "C");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

游标的逻辑:初始时游标指向”第一个之前”,hasNext() 判断后面是否还有,next() 取出当前并前移。

4.3 增强for循环

Java 5 的增强 for 循环(for-each)就是迭代器的语法糖——任何实现了 Iterable 的对象都能用:

for (String s : list) {
    System.out.println(s);
}

编译器把它翻译成等价的迭代器代码。所以所有 Collection 都能用 for-each,连数组也行(数组走的是下标版本)。

4.4 迭代时删除元素

一个经典陷阱:边遍历边删除,会抛 ConcurrentModificationException

List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer n : list) {
    if (n % 2 == 0) list.remove(n);   // ❌ 抛异常!
}

for-each 用的是迭代器,但你调的是 list.remove()——迭代器发现列表被”外部”修改了,立刻翻脸。

正确做法是用迭代器自己的 remove()

Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    if (it.next() % 2 == 0) it.remove();   // ✅ 安全
}

迭代器的 remove() 会同步更新自己的”修改计数”,不会触发异常。Java 8 之后更推荐用 removeIf

list.removeIf(n -> n % 2 == 0);   // ✅ 内部安全删除

4.5 fail-fast 与 fail-safe

Java 集合的迭代器大多是 fail-fast(快速失败)的——一旦发现并发修改,立刻抛异常,宁可”君子之交淡如水”也不”默默将错就错”。ArrayListHashMap 都属于这类。

而并发集合(CopyOnWriteArrayListConcurrentHashMap)的迭代器是 fail-safe(安全失败)的——它们遍历的是创建时的快照或弱一致性视图,不抛异常,但可能看不到最新修改。

五、集合选型指南

面对一堆容器,新手常犯难:“我到底该用哪个?“其实选型有清晰的思路。

5.1 第一步:要键值对吗?

  • → 用 Map
    • 需要按 key 排序?→ TreeMap
    • 需要保持插入顺序 / LRU?→ LinkedHashMap
    • 高并发?→ ConcurrentHashMap
    • 默认 → HashMap

5.2 第二步:要唯一性吗?

  • (不允许重复)→ 用 Set
    • 需要排序?→ TreeSet
    • 需要保持插入顺序?→ LinkedHashSet
    • 默认 → HashSet

5.3 第三步:要按特定顺序处理吗?

  • (先进先出、优先级、栈)→ 用 Queue/Deque
    • 优先级(堆)?→ PriorityQueue
    • 栈 / 双端队列?→ ArrayDeque
    • 阻塞等待?→ ArrayBlockingQueue

5.4 第四步:用 List

  • 默认 → ArrayList(综合最强)
  • 频繁在头部插入删除?→ LinkedList
  • 读多写少且并发?→ CopyOnWriteArrayList

5.5 选型速查表

场景推荐容器
通用列表ArrayList
频繁头插头删LinkedList(或 ArrayDeque 当栈)
去重HashSet
去重 + 排序TreeSet
去重 + 保序LinkedHashSet
键值映射HashMap
键值映射 + 排序TreeMap
键值映射 + 保序LinkedHashMap
并发 MapConcurrentHashMap
优先级处理PriorityQueue
ArrayDeque(不要用 Stack
阻塞队列ArrayBlockingQueue / LinkedBlockingQueue

💡 为什么不用 Stack Stack 继承自 Vector,所有方法都加了 synchronized,性能差,且设计上”暴露了太多 List 的方法”。官方推荐用 ArrayDeque 当栈——它没有这些历史包袱,更快更干净。

5.6 一个万能默认

如果你完全不确定,就记住:ArrayList + HashMap。它们俩覆盖了 90% 的日常场景,性能均衡,几乎不会让你后悔。等遇到瓶颈再换——这才是务实的工程态度。

六、集合与数组的转换

数组与集合之间常常需要互转,这里有几个常被忽视的坑。

6.1 集合转数组:toArray

List<String> list = List.of("A", "B", "C");

// 方式一:返回 Object[],丢失类型
Object[] arr1 = list.toArray();

// 方式二:传入类型一致的数组(推荐)
String[] arr2 = list.toArray(new String[0]);
// 或指定大小
String[] arr3 = list.toArray(new String[list.size()]);

为什么推荐传 new String[0]?传一个空数组,集合内部会自动创建一个大小匹配的数组返回。传 new String[list.size()] 也行,但现代 JVM 对 new String[0] 这种写法做了优化,性能反而更好。

注意:不能直接强转 Object[]String[]

String[] bad = (String[]) list.toArray();   // ❌ ClassCastException

因为 toArray() 真的返回 Object[],运行时不是 String[]

6.2 数组转集合:Arrays.asList

String[] arr = {"A", "B", "C"};
List<String> list = Arrays.asList(arr);

坑一:返回的是固定大小的视图,不能增删。

list.add("D");      // ❌ UnsupportedOperationException
list.remove(0);     // ❌ UnsupportedOperationException
list.set(0, "X");   // ✅ 可以修改元素(会改到原数组!)

Arrays.asList 返回的是 Arrays$ArrayList(内部类,不是 java.util.ArrayList),它直接引用原数组,不支持结构修改。修改它的元素会同步反映到原数组——它们共享存储。

坑二:基本类型数组的坑。

int[] nums = {1, 2, 3};
List<int[]> list = Arrays.asList(nums);   // ⚠️ List<int[]>,不是 List<Integer>!
System.out.println(list.size());           // 1(整个数组被当作一个元素)

因为 Arrays.asList(T... a)T 不能是基本类型,int[] 整体被当成一个 Object。要用 Integer[]

Integer[] nums = {1, 2, 3};
List<Integer> list = Arrays.asList(nums);   // ✅ List<Integer>,size=3

6.3 转成真正的 ArrayList

如果想要一个能自由增删的 ArrayList

// Java 8+
List<String> list = new ArrayList<>(Arrays.asList(arr));

// Java 9+(不可变)
List<String> immutable = List.of(arr);

// Java 10+(不可变副本)
List<String> copy = List.copyOf(Arrays.asList(arr));

// Stream 方式
List<String> streamList = Arrays.stream(arr).collect(Collectors.toList());

七、实战:把本章知识串起来

下面用一个综合示例,展示集合选型与互转的常见操作:

Java · 在线运行

这个例子串起了:数组互转、去重(Set 三种变体)、词频(Map)、迭代器安全删除、Queue 当队列、Deque 当栈。每个场景都选了最合适的容器——这就是”选型”的实战意义。

八、本章小结

主题要点
框架组成Collection(List/Set/Queue)+ Map 两大族谱
List有序、可重复、可索引
Set无序(或保序/排序)、不可重复
Queue/Deque按特定顺序处理元素
Map键值对映射,按 key 查 value
Iterable可迭代的契约,iterator() 返回 Iterator
IteratorhasNext() / next() / remove()
for-eachIterable 的语法糖
fail-fast检测并发修改,抛 ConcurrentModificationException
toArray(T[])集合转数组,推荐传 new T[0]
Arrays.asList数组转集合视图,固定大小,不能增删
选型默认ArrayList + HashMap 覆盖 90% 场景

结语:登高望远

这一章是集合框架的”地图”——我们站在高处俯瞰了整个体系的脉络,记住了两大族谱的名字,了解了迭代器的统一遍历之道,也揣摩了一份选型速查表。

但地图终究是地图,真正的风景要靠脚走出来。接下来的章节,我们将逐一深入:List 的动态数组与链表之争、Set 的去重原理、Map 的哈希之妙、Queue 的进出之道、Collections 工具的百宝箱,以及让集合脱胎换骨的 Stream API

Java 集合框架是这门语言最引以为傲的设计之一——它的优雅、统一与可扩展性,是无数 Java 程序员日常生产力的基石。让我们开始这段旅程。