Appearance
Java 集合框架 — 系统学习笔记
一、概述
Java 集合框架(Java Collections Framework, JCF) 是 java.util 包中提供的一套统一的数据结构接口与实现,用于存储和操作对象集合。它解决了数组长度固定、类型不安全、缺乏丰富操作等痛点,是 Java 开发中使用最频繁的 API 之一。
核心价值:提供可复用的数据结构和算法,让开发者专注于业务逻辑而非底层数据组织。
集合框架的接口继承层次:
mermaid
graph TD
Iterable --> Collection
Collection --> List
Collection --> Set
Collection --> Queue
Queue --> Deque
Set --> SortedSet
SortedSet --> NavigableSet
Map --> SortedMap
SortedMap --> NavigableMap
List -.->|实现| ArrayList
List -.->|实现| LinkedList
List -.->|实现| CopyOnWriteArrayList
Set -.->|实现| HashSet
Set -.->|实现| LinkedHashSet
NavigableSet -.->|实现| TreeSet
Deque -.->|实现| ArrayDeque
Queue -.->|实现| PriorityQueue
Map -.->|实现| HashMap
Map -.->|实现| LinkedHashMap
Map -.->|实现| ConcurrentHashMap
NavigableMap -.->|实现| TreeMap关键区分:
Collection是所有单列集合的根接口;Map是键值对集合的根接口,二者是平行关系,Map 并不继承 Collection。
二、核心概念与原理
2.1 Collection 接口
Collection 接口定义了对集合进行基本操作的方法(add、remove、contains、size、isEmpty、iterator 等),所有实现类必须提供:
- 无参构造函数:创建空集合
- 带 Collection 参数的构造函数:用已有集合创建新集合
易混淆点:
Collection是接口,Collections是工具类(提供排序、查找、同步包装等静态方法)。
2.2 Iterator 与快速失败机制
Iterator 是遍历集合的统一方式,核心方法:hasNext()、next()、remove()。
快速失败(fail-fast):
ArrayList、HashMap等非并发集合内部维护modCount计数器- 迭代过程中检测到
modCount与期望值不一致时,立即抛出ConcurrentModificationException - 目的:尽早暴露并发修改错误,避免数据不一致
安全失败(fail-safe):
CopyOnWriteArrayList、ConcurrentHashMap等并发集合- 遍历的是快照或不抛异常,但可能看到旧数据(弱一致性)
遍历时正确删除元素的方式:
java
// ✅ 方式一:使用 Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("target".equals(it.next())) {
it.remove(); // 正确维护 modCount
}
}
// ✅ 方式二:Java 8+ removeIf
list.removeIf(item -> "target".equals(item));
// ❌ 错误方式:for-each 中直接调用 list.remove()
for (String item : list) {
if ("target".equals(item)) {
list.remove(item); // 触发 ConcurrentModificationException
}
}2.3 Iterable 与 for-each 语法糖
- 实现
Iterable接口的类可使用for-each,编译器会将其展开为Iterator调用 - 数组的
for-each编译为下标访问(无 Iterator 开销) - 自定义数据结构实现
Iterable可无缝融入 Stream、Collections 工具类等生态
2.4 泛型与通配符
| 通配符 | 语义 | 读写特性 | 示例 |
|---|---|---|---|
? | 无界 | 只能读为 Object | List<?> |
? extends T | 上界 | 只读(Producer) | List<? extends Number> |
? super T | 下界 | 只写(Consumer) | List<? super Integer> |
PECS 原则:Producer Extends, Consumer Super。从集合取数据用
extends,往集合写数据用super。
类型擦除:Java 泛型在编译后擦除类型信息,运行时 List<String> 和 List<Integer> 均为 List。
三、List:有序可重复集合
3.1 ArrayList
3.1.1 底层结构与扩容机制
底层:Object[] 动态数组。
扩容流程(JDK 8):
mermaid
flowchart TD
A["调用 add(e)"] --> B{"table == null ?"}
B -->|是| C["首次初始化,容量 = 10"]
B -->|否| D{"size + 1 > capacity ?"}
D -->|否| E["直接赋值 elementData[size++] = e"]
D -->|是| F["调用 grow()"]
F --> G["newCapacity = oldCapacity + oldCapacity >> 1(1.5倍)"]
G --> H["Arrays.copyOf 复制到新数组"]
H --> E关键源码:
java
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量 = 旧容量 * 1.5(位运算高效)
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}最佳实践:已知元素数量时,使用
new ArrayList<>(expectedSize)预分配容量,避免频繁扩容(每次扩容是 $O(n)$ 的数组拷贝)。
3.1.2 时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
get(i) / set(i, v) | $O(1)$ | 数组下标直接寻址 |
add(e)(尾部) | 均摊 $O(1)$ | 偶发扩容 |
add(i, e)(中间) | $O(n)$ | System.arraycopy 移位 |
remove(i) | $O(n)$ | 后续元素前移 |
contains(v) | $O(n)$ | 线性扫描 |
3.1.3 subList 的视图陷阱
java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(1, 3); // [B, C],共享底层数组
sub.set(0, "X"); // list 变为 [A, X, C, D]
list.add("E"); // 结构修改后,再访问 sub 将抛 ConcurrentModificationException
// ✅ 需要独立副本时:
List<String> safeSub = new ArrayList<>(list.subList(1, 3));3.2 LinkedList
底层:双向链表(每个 Node 含 prev/next 指针),同时实现 List 和 Deque 接口。
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 头尾插删 | $O(1)$ | 直接操作 head/tail 指针 |
| 中间插删 | $O(1)$(定位后) | 但定位需 $O(n)$ 遍历 |
随机访问 get(i) | $O(n)$ | 需遍历链表 |
实际性能:在大多数场景下 LinkedList 性能不如 ArrayList,原因是链表节点内存不连续,CPU 缓存不友好,且插删前需 $O(n)$ 定位。
作为栈/队列:LinkedList 实现了 Deque 接口,可用作双端队列、栈、队列。但 Java 官方文档明确建议:
- 栈:优先用
ArrayDeque(数组实现,无锁,性能更好) - 队列:优先用
ArrayDeque或LinkedBlockingQueue(并发场景) - 不推荐
Stack类:继承 Vector,同步开销大
3.3 CopyOnWriteArrayList
写时复制:每次写操作(add/set/remove)都复制整个底层数组(加 ReentrantLock 锁),写完后原子性替换引用;读操作完全无锁。
java
// 适用场景:读多写极少(如监听器列表、配置项)
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("listener1");
// 迭代过程中修改不会抛 ConcurrentModificationException
for (String listener : listeners) {
listeners.add("newListener"); // 操作的是副本,迭代器看到的是旧快照
}| 优点 | 缺点 |
|---|---|
| 读无锁,并发读性能极高 | 写操作内存开销大(整体复制) |
| 迭代器不抛 CME | 数据弱一致性(读到旧版本) |
| 天然线程安全 | 写频繁场景 GC 压力剧增 |
3.4 ArrayList vs LinkedList 对比
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | $O(1)$,快 | $O(n)$,慢 |
| 尾部增删 | 均摊 $O(1)$ | $O(1)$ |
| 中间增删 | $O(n)$(数组拷贝) | $O(n)$(遍历定位) |
| 内存占用 | 紧凑(连续内存) | 每个节点额外存储两个指针 |
| CPU 缓存 | 友好 | 不友好 |
| 线程安全 | 否 | 否 |
结论:绝大多数场景优先选择 ArrayList。
四、Set:无序不重复集合
4.1 HashSet
底层:HashMap(key 存元素,value 存固定的 PRESENT 对象)。
元素唯一性保证:hashCode() → 定位桶 → equals() → 判断是否重复。
⚠️ 关键:放入 HashSet 的对象必须正确重写
hashCode()和equals(),否则会出现"逻辑重复"元素。
java
// 错误示例:未重写 hashCode 和 equals
class User {
String name;
// 两个 name 相同的 User 会被当作不同元素存入 HashSet
}链表转红黑树的阈值机制
当同一个桶的链表长度 $\geq 8$(TREEIFY_THRESHOLD)且数组总容量 $\geq 64$(MIN_TREEIFY_CAPACITY)时,链表转为红黑树,查找复杂度从 $O(n)$ 降至 $O(\log n)$。当红黑树节点数 $\leq 6$(UNTREEIFY_THRESHOLD)时退化回链表。
为何选红黑树而非 AVL 树? 红黑树的旋转次数上限是常数(插入 $\leq 2$ 次,删除 $\leq 3$ 次),而 AVL 树为 $O(\log n)$ 次。在 HashMap 插删频繁的场景下,红黑树的写性能更优。
4.2 TreeSet
- 底层:
TreeMap(红黑树),元素按自然顺序或指定Comparator排序 - 增删查均为 $O(\log n)$
- 独特能力:范围查询(
headSet/tailSet/subSet) - 元素必须实现
Comparable或构造时传入Comparator
4.3 LinkedHashSet
- 底层:
LinkedHashMap,在 HashSet 基础上维护双向链表 - 遍历时按插入顺序输出,查找仍 $O(1)$
- 内存开销略高于 HashSet
五、Queue 与 Deque
5.1 Queue 接口
代表 FIFO(先进先出)队列:
| 操作 | 抛异常 | 返回特殊值 |
|---|---|---|
| 插入 | add(e) | offer(e) |
| 移除 | remove() | poll() |
| 检查 | element() | peek() |
5.2 ArrayDeque
- 底层:循环动态数组,可自动扩容
- 既可作队列(FIFO),也可作栈(LIFO)
- 不允许 null 元素
- 性能优于
LinkedList和Stack
java
// 作为栈使用
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 压栈
stack.push(2);
stack.pop(); // 弹栈,返回 2
stack.peek(); // 查看栈顶,返回 1
// 作为队列使用
Deque<String> queue = new ArrayDeque<>();
queue.offer("A"); // 入队
queue.offer("B");
queue.poll(); // 出队,返回 A5.3 PriorityQueue
- 底层:二叉堆(Binary Heap)
- 元素按自然顺序或
Comparator排序,队首始终是最小元素 - 访问最小元素 $O(1)$,插入/移除 $O(\log n)$
- 迭代器不保证有序遍历
- 适用场景:任务调度、Top-K 问题
5.4 阻塞队列(BlockingQueue / BlockingDeque)
属于 java.util.concurrent 包,用于生产者-消费者模式:
| 实现类 | 底层 | 有界/无界 | 特点 |
|---|---|---|---|
ArrayBlockingQueue | 数组 | 有界 | 公平/非公平锁可选 |
LinkedBlockingQueue | 链表 | 可选有界 | 吞吐量通常高于 Array 版 |
PriorityBlockingQueue | 堆 | 无界 | 支持优先级 |
LinkedBlockingDeque | 链表 | 可选有界 | 双端阻塞队列 |
java
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
// 生产者
queue.put("task"); // 队列满时阻塞
// 消费者
String task = queue.take(); // 队列空时阻塞六、Map:键值对映射
6.1 HashMap 底层结构与原理(JDK 8)
6.1.1 数据结构
数组 + 链表 + 红黑树:
Node[] table (默认16)
├── [0] → null
├── [1] → Node → Node → null (链表)
├── [2] → TreeNode(红黑树根) (链表长度≥8 且 数组≥64 时转换)
├── ...
└── [15] → Node → null6.1.2 put 流程
mermaid
flowchart TD
A["put(key, value)"] --> B["hash = (h = key.hashCode()) ^ (h >>> 16)"]
B --> C{"table == null ?"}
C -->|是| D["resize() 初始化,容量16"]
C -->|否| E["i = (n-1) & hash 定位桶"]
D --> E
E --> F{"tab[i] == null ?"}
F -->|是| G["直接放入新 Node"]
F -->|否| H{"key 相同?(hash相等 && equals)"}
H -->|是| I["覆盖 value,返回旧值"]
H -->|否| J{"是 TreeNode?"}
J -->|是| K["红黑树插入"]
J -->|否| L["遍历链表尾部插入"]
L --> M{"链表长度 ≥ 8?"}
M -->|是| N{"数组长度 ≥ 64?"}
N -->|是| O["链表转红黑树"]
N -->|否| P["resize() 扩容"]
M -->|否| Q["插入完成"]
G --> R{"++size > threshold?"}
K --> R
O --> R
Q --> R
R -->|是| S["resize() 扩容"]
R -->|否| T["返回 null"]6.1.3 哈希扰动函数
java
static final int hash(Object key) {
int h;
// 高16位与低16位异或,使高位也参与桶位置计算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}目的:桶位置只用低位 bits((n-1) & hash),扰动后让高位也参与,减少哈希碰撞。
6.1.4 扩容机制(resize)
- 触发条件:$size > threshold$($= capacity \times loadFactor = 16 \times 0.75 = 12$)
- 扩容规则:容量翻倍($capacity \ll 1$)
- 元素重定位:由于容量是 2 的幂,节点新位置要么在原下标,要么在原下标 + 旧容量处(由
hash & oldCap的结果决定),无需全量重新计算 hash
java
// 扩容时元素重定位的核心逻辑
if ((e.hash & oldCap) == 0)
// 留在原位置
else
// 移动到 原位置 + oldCap最佳实践:已知元素数量时,初始容量设为
expectedSize / 0.75 + 1,避免频繁扩容。
6.1.5 为何数组长度必须是 2 的幂?
- 计算索引高效:
(n - 1) & hash等价于hash % n,但位运算更快 - 扩容时重定位高效:只需判断
hash & oldCap的一个 bit 即可
6.2 ConcurrentHashMap
JDK 7 vs JDK 8 实现对比
| 维度 | JDK 7 | JDK 8 |
|---|---|---|
| 锁机制 | 分段锁(Segment 数组) | CAS + synchronized 节点锁 |
| 锁粒度 | 每段一把锁(默认16段) | 单个桶(Node)一把锁 |
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 并发度 | 最多16线程 | 理论上无上限 |
JDK 8 实现细节
java
// put 操作
// 1. 桶为空 → CAS 无锁写入
// 2. 桶非空 → 对头节点加 synchronized 锁,再操作链表/红黑树
// get 操作 → 完全无锁(Node.val 和 next 用 volatile 修饰)
// size() → 通过分散的 CounterCell 累加(类似 LongAdder)弱一致性读:迭代器和 size() 不提供实时快照,但不抛 ConcurrentModificationException。这是用弱一致性换取高并发吞吐量的设计权衡。
6.3 LinkedHashMap 与 LRU 缓存
LinkedHashMap 在 HashMap 基础上维护双向链表,支持两种顺序:
- 插入顺序(默认)
- 访问顺序(
accessOrder=true,每次 get/put 将节点移到链表尾部)
手写 LRU 缓存(面试高频):
java
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
// accessOrder=true:按访问顺序排列
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当元素数量超过容量时,自动移除最久未访问的元素(链表头部)
return size() > capacity;
}
}
// 使用
LRUCache<String, Integer> cache = new LRUCache<>(3);
cache.put("a", 1);
cache.put("b", 2);
cache.put("c", 3);
cache.get("a"); // a 移到尾部
cache.put("d", 4); // 触发移除最久未访问的 b
System.out.println(cache); // {c=3, a=1, d=4}6.4 TreeMap
- 底层:红黑树,key 按自然顺序或 Comparator 排序
- 所有操作 $O(\log n)$
- 独特 API:
firstKey()/lastKey()、headMap()/tailMap()/subMap()、floorKey()/ceilingKey() - 适用场景:需要按 key 排序遍历或范围查询
6.5 Map 实现类对比
| 集合 | 线程安全 | 底层 | null key | null value | 默认容量 | 扩容 |
|---|---|---|---|---|---|---|
| HashMap | 否 | 数组+链表/红黑树 | 是(1个) | 是 | 16 | 2倍 |
| LinkedHashMap | 否 | HashMap+双向链表 | 是(1个) | 是 | 16 | 2倍 |
| TreeMap | 否 | 红黑树 | 否 | 是 | - | - |
| Hashtable | 是 | 数组+链表 | 否 | 否 | 11 | 2倍+1 |
| ConcurrentHashMap | 是 | 数组+链表/红黑树 | 否 | 否 | 16 | 2倍 |
七、底层数据结构速览
7.1 数组
- 连续内存空间,支持 $O(1)$ 随机访问
- 寻址公式:$a[i] = baseAddress + i \times dataTypeSize$
- 索引从 0 开始可以减少一次减法运算(偏移量即下标)
- 插入/删除平均 $O(n)$,头尾最优 $O(1)$
7.2 链表
| 类型 | 查询 | 头部操作 | 尾部操作 | 给定节点操作 |
|---|---|---|---|---|
| 单向链表 | 头 $O(1)$,其他 $O(n)$ | $O(1)$ | $O(n)$ | - |
| 双向链表 | 头尾 $O(1)$,其他 $O(n)$ | $O(1)$ | $O(1)$ | $O(1)$ |
7.3 散列表(Hash Table)
- 通过散列函数将 key 映射为数组下标
- 散列冲突解决:
- 拉链法(链表/红黑树挂在桶上)— HashMap 使用此方式
- 开放寻址法(线性探测、二次探测等)
将链表改造为红黑树还有一个重要原因:防止 HashDoS 攻击。攻击者构造大量相同 hash 的 key,使链表退化为 $O(n)$;红黑树将其限制在 $O(\log n)$。
7.4 红黑树
红黑树是自平衡二叉搜索树,五大性质:
- 节点是红色或黑色
- 根节点是黑色
- 叶子节点(NIL)是黑色
- 红色节点的子节点必须是黑色(不能有连续红节点)
- 从任一节点到叶子的所有路径包含相同数量的黑色节点
时间复杂度:查找、插入、删除均为 $O(\log n)$。
八、集合工具类与最佳实践
8.1 Collections 工具类
java
// 排序(TimSort,稳定排序)
Collections.sort(list);
Collections.sort(list, Comparator.reverseOrder());
// 二分查找(需预排序)
int index = Collections.binarySearch(list, target);
// 不可变视图(修改抛 UnsupportedOperationException)
List<String> unmodifiable = Collections.unmodifiableList(list);
// 同步包装(粗粒度锁,迭代仍需手动同步,不推荐)
List<String> syncList = Collections.synchronizedList(list);
// 其他
Collections.shuffle(list); // 随机打乱
Collections.frequency(list, element); // 元素出现次数8.2 Java 9+ 不可变集合工厂方法
java
// 创建真正不可变集合(不允许 null,修改抛 UnsupportedOperationException)
List<Integer> list = List.of(1, 2, 3);
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("k1", 1, "k2", 2);
// 超过10个键值对
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("k1", 1),
Map.entry("k2", 2)
// ...
);
// Java 10+ 创建现有集合的不可变副本
List<String> copy = List.copyOf(mutableList);不可变集合天然线程安全,推荐在常量、函数参数边界处使用。
8.3 集合选型决策树
mermaid
flowchart TD
START["需要存储什么?"] --> KV{"键值对?"}
KV -->|是| SORT_MAP{"需要排序?"}
SORT_MAP -->|是| TREEMAP["TreeMap"]
SORT_MAP -->|否| ORDER_MAP{"需要插入/访问顺序?"}
ORDER_MAP -->|是| LINKEDHASHMAP["LinkedHashMap"]
ORDER_MAP -->|否| CONC_MAP{"高并发?"}
CONC_MAP -->|是| CONCURRENTHASHMAP["ConcurrentHashMap"]
CONC_MAP -->|否| HASHMAP["HashMap"]
KV -->|否| DUP{"允许重复?"}
DUP -->|否| SORT_SET{"需要排序?"}
SORT_SET -->|是| TREESET["TreeSet"]
SORT_SET -->|否| ORDER_SET{"需要插入顺序?"}
ORDER_SET -->|是| LINKEDHASHSET["LinkedHashSet"]
ORDER_SET -->|否| HASHSET["HashSet"]
DUP -->|是| FIFO{"需要 FIFO/LIFO?"}
FIFO -->|是| ARRAYDEQUE["ArrayDeque"]
FIFO -->|否| ACCESS{"随机访问多?"}
ACCESS -->|是| ARRAYLIST["ArrayList"]
ACCESS -->|否| LINKEDLIST["LinkedList(慎用)"]总则:单线程优先 ArrayList/HashMap,多线程用并发集合,不要用
Collections.synchronized*(粗粒度锁性能差)。
8.4 Comparable vs Comparator
| 维度 | Comparable | Comparator |
|---|---|---|
| 包 | java.lang | java.util |
| 方法 | compareTo(T o) | compare(T o1, T o2) |
| 修改类 | 需要修改排序类本身 | 不需要,外部定义 |
| 排序策略 | 单一(自然排序) | 多种(可定义多个 Comparator) |
| 绑定方式 | 静态绑定 | 动态绑定 |
java
// Comparator 多种排序策略
Comparator<Employee> byName = Comparator.comparing(Employee::getName);
Comparator<Employee> byAge = Comparator.comparingInt(Employee::getAge);
Comparator<Employee> byDeptThenName = Comparator
.comparing(Employee::getDepartment)
.thenComparing(Employee::getName);
Collections.sort(employees, byDeptThenName);九、面试高频问题
Q1:HashMap 的实现原理?JDK 7 与 JDK 8 的区别?
答:
- 底层使用数组 + 链表 + 红黑树(JDK 8+),数组是主体,链表/红黑树解决哈希冲突
- JDK 7:数组 + 链表,头插法,多线程下可能出现死循环(resize 时链表成环)
- JDK 8:数组 + 链表 + 红黑树,尾插法,链表长度 $\geq 8$ 且数组 $\geq 64$ 时转红黑树,修复了死循环但仍线程不安全
Q2:HashMap 的 put 流程?
答:
- 判断 table 是否为空,空则 resize 初始化
- 计算 key 的 hash(扰动函数),通过
(n-1) & hash定位桶 - 桶为空 → 直接放入新节点
- 桶非空 → 判断首节点 key 是否相同 → 相同则覆盖 value
- 不同 → 判断是否为 TreeNode → 是则红黑树插入
- 否则遍历链表尾插 → 插入后检查链表长度是否 $\geq 8$ → 转红黑树
- 插入后
++size > threshold则扩容
Q3:HashMap 的扩容机制?
答:
- 首次 add 初始化容量 16,后续
size > capacity × 0.75时扩容 - 容量翻倍($capacity \ll 1$)
- 扩容后元素重新定位:
hash & oldCap == 0留原位,否则移到原位 + oldCap - 扩容是 $O(n)$ 操作,建议提前设置合理初始容量
Q4:ArrayList 和 LinkedList 的区别?
答:
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层 | 动态数组 | 双向链表 |
| 随机访问 | $O(1)$ | $O(n)$ |
| 尾部增删 | 均摊 $O(1)$ | $O(1)$ |
| 中间增删 | $O(n)$ | $O(n)$(遍历定位) |
| 内存 | 紧凑 | 每节点额外两指针 |
| 缓存友好 | 是 | 否 |
结论:绝大多数场景优先 ArrayList。
Q5:ConcurrentHashMap 如何保证线程安全?
答:
- JDK 7:分段锁(Segment),每段独立 ReentrantLock,最多 16 线程并发
- JDK 8:取消分段锁,改用 CAS + synchronized:
- 桶为空 → CAS 无锁写入
- 桶非空 → 对头节点加 synchronized
- get 完全无锁(volatile 保证可见性)
- size 通过 CounterCell 分散累加(类似 LongAdder)
十、总结
| 类别 | 推荐实现 | 核心特征 |
|---|---|---|
| 有序可重复 | ArrayList | 动态数组,随机访问 $O(1)$,扩容 1.5 倍 |
| 频繁头尾操作 | ArrayDeque | 循环数组双端队列,替代 Stack/LinkedList |
| 去重无序 | HashSet | 底层 HashMap,$O(1)$ 查找 |
| 去重有序 | TreeSet | 红黑树,$O(\log n)$,支持范围查询 |
| 键值对 | HashMap | 数组+链表+红黑树,$O(1)$ 均摊 |
| 有序键值对 | TreeMap | 红黑树,按 key 排序 |
| 并发 Map | ConcurrentHashMap | CAS+synchronized,高并发首选 |
| 并发 List | CopyOnWriteArrayList | 写时复制,读多写少场景 |
| LRU 缓存 | LinkedHashMap | accessOrder=true + removeEldestEntry |
| 优先级队列 | PriorityQueue | 二叉堆,队首最小元素 |
| 不可变集合 | List.of() / Map.of() | Java 9+,天然线程安全 |
核心原则:
- 选对集合类型是性能优化的第一步
- 理解底层数据结构(数组、链表、红黑树、哈希表)是写出正确代码的基础
- 单线程用标准集合,多线程用
java.util.concurrent下的并发集合 - 避免使用
Vector、Stack、Hashtable、Collections.synchronized*等过时方案