主题
Chapter 14 · 集合框架(Collections Framework)
数组太死板,Java 集合是"会自动伸缩的容器"。
🎯 本章目标
- 理解 集合框架的整体结构
- 掌握
List、Set、Queue、Map的核心实现类 - 学会选用合适的集合(性能 / 排序 / 唯一性)
- 用对 迭代器(Iterator)
- 知道
ArrayListvsLinkedList、HashMapvsTreeMap的区别
1. 为什么需要集合?
数组的痛点:
- 长度固定(声明就死)
- 类型单一
- 操作笨重(删除一个元素要手动挪动)
集合解决:
- 动态伸缩
- 提供丰富 API(增删改查、排序、查找)
- 配合泛型类型安全
2. 集合框架全景
Iterable
│
┌─────┴─────┐
Collection Map
┌─────┼─────┐ ┌──┼──┐
List Set Queue HashMap TreeMap LinkedHashMap
│ │ │
ArrayList HashSet LinkedList
LinkedList TreeSet PriorityQueue
Vector LinkedHashSet💡 注意:Map 不属于 Collection!它是单独的体系。
3. List:有序、可重复
| 实现类 | 底层 | 特点 |
|---|---|---|
| ArrayList ⭐ | 动态数组 | 查找快 O(1),增删慢 O(n) |
| LinkedList | 双向链表 | 增删快 O(1),查找慢 O(n) |
| Vector | 数组(synchronized) | 线程安全(很少用,用 CopyOnWriteArrayList 代替) |
常用操作
java
List<String> list = new ArrayList<>();
list.add("a"); // 末尾添加
list.add(0, "x"); // 指定位置插入
list.set(1, "b"); // 修改
list.get(0); // 获取
list.remove("a"); // 按对象删
list.remove(0); // 按下标删
list.size();
list.contains("b");
list.isEmpty();
list.indexOf("b");
for (String s : list) { ... } // for-each
list.forEach(System.out::println); // JDK 8 forEach
List.of("a", "b", "c"); // JDK 9+ 不可变 ListArrayList vs LinkedList
| 操作 | ArrayList | LinkedList |
|---|---|---|
随机访问 get(i) | O(1) ✓ | O(n) |
| 末尾添加 | O(1) 摊销 | O(1) |
| 中间插入 / 删除 | O(n)(要挪动) | O(1)(节点切换) |
| 内存 | 紧凑 | 每个节点要存指针 |
经验:90% 场景用 ArrayList,频繁头尾增删才用 LinkedList。
4. Set:唯一不重复
| 实现类 | 底层 | 特点 |
|---|---|---|
| HashSet ⭐ | HashMap | 无序,O(1),最常用 |
| LinkedHashSet | LinkedHashMap | 按插入顺序 |
| TreeSet | 红黑树 | 自动排序 O(log n) |
java
Set<String> set = new HashSet<>();
set.add("a");
set.add("a"); // 重复,不加入
System.out.println(set.size()); // 1
set.contains("a"); // O(1)💡 Set 去重的关键:元素必须正确实现
equals()+hashCode()!
5. Queue & Deque:队列与双端队列
java
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队
queue.offer(2);
Integer head = queue.poll(); // 出队(队头)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 入栈
stack.push(2);
Integer top = stack.pop(); // 出栈
PriorityQueue<Integer> pq = new PriorityQueue<>(); // 默认小顶堆
pq.offer(3); pq.offer(1); pq.offer(2);
while (!pq.isEmpty()) System.out.println(pq.poll()); // 1 2 3💡 Stack 类已过时,用 ArrayDeque 当栈用!
6. Map:键值对
| 实现类 | 底层 | 特点 |
|---|---|---|
| HashMap ⭐ | 数组+链表+红黑树 | 无序,O(1),最常用 |
| LinkedHashMap | 同上 + 双向链表 | 按插入顺序 |
| TreeMap | 红黑树 | 按 key 自动排序 O(log n) |
| Hashtable | 数组+链表 | 线程安全(已过时,用 ConcurrentHashMap) |
常用操作
java
Map<String, Integer> map = new HashMap<>();
map.put("apple", 5);
map.put("banana", 3);
map.put("apple", 10); // 覆盖
map.get("apple"); // 10
map.containsKey("apple"); // true
map.containsValue(3);
map.size();
map.remove("banana");
// JDK 8+ 增强方法
map.getOrDefault("cherry", 0);
map.putIfAbsent("apple", 999); // 已存在不覆盖
map.computeIfAbsent("cherry", k -> 0);
map.merge("apple", 1, Integer::sum); // 累加
// 遍历
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
map.forEach((k, v) -> System.out.println(k + " -> " + v));
map.keySet();
map.values();
// JDK 9+ 不可变 Map
Map.of("a", 1, "b", 2);HashMap 内部结构(高频面试)
- JDK 8 之前:数组 + 链表
- JDK 8 之后:数组 + 链表 + 红黑树(链表长度 ≥ 8 时转红黑树)
- 默认初始容量 16,加载因子 0.75
- key 用
hashCode()算桶位置,用equals()在桶内比较
7. 集合选型指南
需要 key-value?
├─ 是 → Map
│ ├─ 要排序 → TreeMap
│ ├─ 要保留插入顺序 → LinkedHashMap
│ └─ 默认 → HashMap ⭐
│
└─ 否 → Collection
├─ 要唯一?
│ ├─ 是 → Set
│ │ ├─ 要排序 → TreeSet
│ │ ├─ 要保留插入顺序 → LinkedHashSet
│ │ └─ 默认 → HashSet ⭐
│ └─ 否 → List
│ ├─ 频繁随机访问 → ArrayList ⭐
│ └─ 频繁头尾增删 → LinkedList / ArrayDeque
└─ 要队列语义?
├─ FIFO → ArrayDeque (作 Queue)
├─ LIFO → ArrayDeque (作 Stack)
└─ 优先级 → PriorityQueue8. Iterator 与并发修改异常
java
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
// ❌ 边遍历边删除(ConcurrentModificationException)
for (String s : list) {
if (s.equals("b")) list.remove(s); // 抛异常!
}
// ✅ 用 Iterator
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("b")) it.remove(); // 安全
}
// ✅ JDK 8+
list.removeIf(s -> s.equals("b"));9. 实战练习
| 文件 | 内容 |
|---|---|
ListDemo.java | List 全部操作 + 性能对比 |
SetDemo.java | Set 三大实现去重 + 排序 |
MapDemo.java | Map 全部操作 + 三大实现对比 |
WordCount.java | 用 Map 实现词频统计 |
StudentManager.java | 综合:学生管理系统 |
10. 浏览器演示
打开 demo.html:
- 集合操作可视化
- ArrayList vs LinkedList 性能对比
- HashMap 内部结构动画
11. 面试可能会问什么?
Q1: ArrayList 和 LinkedList 区别?
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层 | 动态数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 头尾增删 | 头 O(n) / 尾 O(1) | O(1) |
| 中间增删 | O(n) | O(n)(找位置) |
| 内存 | 紧凑 | 每节点带指针 |
结论:90% 用 ArrayList。
Q2: HashMap 底层数据结构?
- JDK 8+:数组 + 链表 + 红黑树
- 链表长度 ≥ 8 → 转红黑树(树化阈值)
- 红黑树节点 ≤ 6 → 退回链表(去树化阈值)
- 默认容量 16,扩容阈值 16 × 0.75 = 12
Q3: HashMap 为什么扩容总是 2 倍?
为了让 (n - 1) & hash(按位与)等价于取模,速度更快。
Q4: HashMap 是线程安全的吗?
不是。多线程下用:
Collections.synchronizedMap(map)(粗暴加锁)ConcurrentHashMap⭐(推荐,分段锁/CAS)
Q5: 重写 equals 为什么要重写 hashCode?
HashMap/HashSet 先按 hashCode 找桶,再用 equals 比较。如果两对象 equals 为 true 但 hashCode 不同,会被存到不同桶,去重失效。
Q6: Iterator 与 for-each 区别?
for-each 底层就是 Iterator。区别:
- for-each 不能调 remove
- Iterator 可以调
it.remove()
Q7: ArrayList 默认初始容量?怎么扩容?
- 初始容量 10(JDK 8)
- 扩容:旧容量 + 旧容量 / 2 = 1.5 倍
🎁 本章小结
✅ List:有序可重复 | ArrayList ⭐ / LinkedList
✅ Set:唯一不重复 | HashSet ⭐ / TreeSet / LinkedHashSet
✅ Queue:FIFO/LIFO | ArrayDeque / PriorityQueue
✅ Map:键值对 | HashMap ⭐ / TreeMap / LinkedHashMap
✅ 重写 equals 必重写 hashCode
✅ 边遍历边删用 Iterator.remove() 或 removeIf🔗 导航
- ⬅️ 上一章:Chapter 13 · 异常处理
- ➡️ 下一章:Chapter 15 · 泛型