集合体系总览
如果变量是”装一个数据的盒子”,那么集合就是”装一堆数据的容器”。一个变量只能记一个电话号,而你通讯录里的几百个联系人怎么办?数组虽然能装多个,但长度固定、增删不便——你想在中间插一个人,得把后面所有人往后挪一位。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);
数组的痛点:
- 长度固定——一旦创建,容量不能变。增删元素要手动搬家。
- API 贫乏——只有
length和下标访问,没有contains、indexOf、sort这些便捷方法。 - 只能存同类型——虽然这有时是优点(类型安全),但缺乏灵活性。
- 无法表达”键值对”——数组是线性的,而很多场景需要”按名字查东西”。
1.2 集合框架的诞生
Java 1.2 引入了集合框架(Collections Framework),它统一了”容器”的抽象:
- 一套接口:
Collection、List、Set、Map、Queue等,定义容器的契约。 - 多套实现:每个接口有多种实现,按场景选择。
- 一套算法:
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(快速失败)的——一旦发现并发修改,立刻抛异常,宁可”君子之交淡如水”也不”默默将错就错”。ArrayList、HashMap 都属于这类。
而并发集合(CopyOnWriteArrayList、ConcurrentHashMap)的迭代器是 fail-safe(安全失败)的——它们遍历的是创建时的快照或弱一致性视图,不抛异常,但可能看不到最新修改。
五、集合选型指南
面对一堆容器,新手常犯难:“我到底该用哪个?“其实选型有清晰的思路。
5.1 第一步:要键值对吗?
- 是 → 用
Map。- 需要按 key 排序?→
TreeMap - 需要保持插入顺序 / LRU?→
LinkedHashMap - 高并发?→
ConcurrentHashMap - 默认 →
HashMap
- 需要按 key 排序?→
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 |
| 并发 Map | ConcurrentHashMap |
| 优先级处理 | 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());
七、实战:把本章知识串起来
下面用一个综合示例,展示集合选型与互转的常见操作:
这个例子串起了:数组互转、去重(Set 三种变体)、词频(Map)、迭代器安全删除、Queue 当队列、Deque 当栈。每个场景都选了最合适的容器——这就是”选型”的实战意义。
八、本章小结
| 主题 | 要点 |
|---|---|
| 框架组成 | Collection(List/Set/Queue)+ Map 两大族谱 |
| List | 有序、可重复、可索引 |
| Set | 无序(或保序/排序)、不可重复 |
| Queue/Deque | 按特定顺序处理元素 |
| Map | 键值对映射,按 key 查 value |
| Iterable | 可迭代的契约,iterator() 返回 Iterator |
| Iterator | hasNext() / next() / remove() |
| for-each | Iterable 的语法糖 |
| fail-fast | 检测并发修改,抛 ConcurrentModificationException |
toArray(T[]) | 集合转数组,推荐传 new T[0] |
Arrays.asList | 数组转集合视图,固定大小,不能增删 |
| 选型默认 | ArrayList + HashMap 覆盖 90% 场景 |
结语:登高望远
这一章是集合框架的”地图”——我们站在高处俯瞰了整个体系的脉络,记住了两大族谱的名字,了解了迭代器的统一遍历之道,也揣摩了一份选型速查表。
但地图终究是地图,真正的风景要靠脚走出来。接下来的章节,我们将逐一深入:List 的动态数组与链表之争、Set 的去重原理、Map 的哈希之妙、Queue 的进出之道、Collections 工具的百宝箱,以及让集合脱胎换骨的 Stream API。
Java 集合框架是这门语言最引以为傲的设计之一——它的优雅、统一与可扩展性,是无数 Java 程序员日常生产力的基石。让我们开始这段旅程。