AI生成-Java ArrayDeque 技术文档
Java ArrayDeque 技术文档
一、概述
ArrayDeque(Array Double Ended Queue)是 Java 集合框架中基于动态循环数组实现的双端队列,同时实现了 Deque 和 Queue 接口。
核心特性
- 双端操作:队首和队尾均可在 O(1) 时间内完成插入与删除
- 基于数组:底层为
Object[],内存连续,缓存友好,遍历性能优于链表 - 无容量上限:动态扩容,默认初始容量 16(实际为 17,见下文)
- 禁止 null:不允许存入
null元素(null在内部被用作哨兵值标记空槽) - 非线程安全:无同步机制,多线程环境需外部同步
与 LinkedList 的对比
| 对比项 | ArrayDeque | LinkedList |
|---|---|---|
| 底层结构 | 循环数组(Object[]) | 双向链表(Node) |
| 内存开销 | 仅数组 + 两个指针,紧凑 | 每个节点额外 2 个指针(prev/next),开销大 |
| 随机访问 | O(1)(数组下标) | O(n)(需遍历链表) |
| 缓存友好性 | ✅ 连续内存,CPU 缓存命中高 | ❌ 链表节点分散,缓存不友好 |
| 扩容代价 | 扩容时需复制数组 | 无扩容,动态分配节点 |
| null 支持 | ❌ 禁止 | ✅ 允许 |
| 实现接口 | Deque, Cloneable, Serializable | Deque, List, Cloneable, Serializable |
| 作为栈使用 | 官方推荐替代 Stack | 可用但性能差 |
| 作为队列使用 | 官方推荐替代 LinkedList | 可用但内存开销大 |
官方建议(JDK 文档):
ArrayDeque用作栈时比Stack更快,用作队列时比LinkedList更快。
二、底层原理
2.1 循环数组结构
ArrayDeque 内部维护三个核心字段:
transient Object[] elements; // 底层循环数组
transient int head; // 队首元素索引(出队方向)
transient int tail; // 队尾下一个插入位置索引
核心思想:head 和 tail 在数组中双向增长,到达边界后绕回到数组另一端,形成逻辑上的环形。
数组物理视图(capacity = 8):
[0] [1] [2] [3] [4] [5] [6] [7]
↑ ↑
head tail
逻辑视图:元素占 [2, 3, 4, 5],head=2, tail=6
队首方向 ← head 递减(取模绕回)
队尾方向 → tail 递增(取模绕回)
索引计算(关键):
// 队首左移(head 递减,绕回)
head = (head - 1) & (elements.length - 1)
// 队尾右移(tail 递增,绕回)
tail = (tail + 1) & (elements.length - 1)
& (elements.length - 1)等价于% elements.length,但要求数组长度必须为 2 的幂- 这就是 ArrayDeque 内部容量始终为 2 的幂的原因
2.2 空与满的判定
空队列:head == tail
满队列:(tail + 1) == head(即 tail 的下一个位置是 head)
- 牺牲一个槽位:tail 指向的位置始终为空(不存元素),因此容量为 n 的数组最多存 n-1 个元素
- 判满条件
(tail + 1) == head等价于(tail + 1) & (mask) == head
2.3 动态扩容机制
触发条件:当 head == tail + 1(即队列已满)时触发扩容。
扩容流程:
- 分配新数组,容量为原来的 2 倍
- 将旧数组元素按逻辑顺序复制到新数组(从 head 开始,绕回复制)
- 重置
head = 0,tail = 旧元素数量
private void doubleCapacity() {
int h = head;
int p = elements.length;
int r = p - h; // head 右侧元素数量
// 新容量 = 旧容量 × 2
Object[] a = new Object[newCapacity];
// 先拷 head 到数组末尾的部分
System.arraycopy(elements, h, a, 0, r);
// 再拷 数组开头到 head 左侧的部分
System.arraycopy(elements, 0, a, r, h);
head = 0;
tail = n;
elements = a;
}
扩容序列:
| 扩容次数 | 数组容量 | 最大元素数 |
|---|---|---|
| 初始 | 16 | 15 |
| 第 1 次 | 32 | 31 |
| 第 2 次 | 64 | 63 |
| 第 3 次 | 128 | 127 |
默认初始容量:构造时传入 numElements,实际容量取 ≥ numElements + 1 的最小 2 的幂。无参构造时 numElements = 16,但需满足 容量 > 元素数,所以初始数组长度为 16,最多存 15 个元素。若构造时指定 16,则 calculateSize(17) → 32。
三、核心 API 分类
3.1 队首操作(First / Head)
| 方法 | 抛异常 | 返回特殊值 | 说明 |
|---|---|---|---|
| 插入 | addFirst(e) |
offerFirst(e) |
在队首插入元素;队列满时 addFirst 抛 IllegalStateException,offerFirst 返回 false |
| 删除 | removeFirst() |
pollFirst() |
移除并返回队首元素;队列为空时 removeFirst 抛 NoSuchElementException,pollFirst 返回 null |
| 查看 | getFirst() |
peekFirst() |
获取但不移除队首元素;队列为空时 getFirst 抛 NoSuchElementException,peekFirst 返回 null |
3.2 队尾操作(Last / Tail)
| 方法 | 抛异常 | 返回特殊值 | 说明 |
|---|---|---|---|
| 插入 | addLast(e) |
offerLast(e) |
在队尾插入元素;队列满时 addLast 抛 IllegalStateException,offerLast 返回 false |
| 删除 | removeLast() |
pollLast() |
移除并返回队尾元素;队列为空时 removeLast 抛 NoSuchElementException,pollLast 返回 null |
| 查看 | getLast() |
peekLast() |
获取但不移除队尾元素;队列为空时 getLast 抛 NoSuchElementException,peekLast 返回 null |
3.3 Queue 视角(FIFO 队列)
| 操作 | 抛异常 | 返回特殊值 | 等价于 |
|---|---|---|---|
| 入队 | add(e) |
offer(e) |
addLast(e) / offerLast(e) |
| 出队 | remove() |
poll() |
removeFirst() / pollFirst() |
| 查看队首 | element() |
peek() |
getFirst() / peekFirst() |
3.4 Stack 视角(LIFO 栈)
| 操作 | 抛异常 | 返回特殊值 | 等价于 |
|---|---|---|---|
| 压栈 | push(e) |
— | addFirst(e) |
| 弹栈 | pop() |
— | removeFirst() |
| 查看栈顶 | peek() |
— | peekFirst() |
3.5 方法命名规律总结
抛异常 返回特殊值(null/false)
插入(Insert) addXxx(e) offerXxx(e)
删除(Remove) removeXxx() pollXxx()
查看(Examine) getXxx() peekXxx()
四、常见应用场景
4.1 替代 Stack 实现栈
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 压栈 → addFirst
stack.push(2);
stack.push(3);
int top = stack.pop(); // 弹栈 → removeFirst,返回 3
int peek = stack.peek(); // 查看栈顶 → peekFirst,返回 2(不弹出)
为什么比 Stack 更好?
Stack继承自Vector,所有方法加synchronized,单线程下性能浪费Stack的push/pop涉及数组尾部操作 + 同步锁,ArrayDeque仅头部指针移动,无锁
4.2 替代 LinkedList 实现 BFS 队列
Deque<Node> queue = new ArrayDeque<>();
queue.offer(root); // 入队 → offerLast
while (!queue.isEmpty()) {
Node node = queue.poll(); // 出队 → pollFirst
for (Node child : node.children) {
queue.offer(child); // 子节点入队
}
}
为什么比 LinkedList 更好?
- 内存连续,CPU 缓存命中率高,遍历更快
- 无节点对象创建/销毁的 GC 压力
- 无额外的 prev/next 指针内存开销
4.3 滑动窗口最大值
// 维护单调递减队列,队首始终为当前窗口最大值
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < nums.length; i++) {
// 移除窗口外的元素
while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 维护单调性:队尾 ≤ 当前值则弹出
while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
if (i >= k - 1) {
result[idx++] = nums[deque.peekFirst()];
}
}
4.4 回文判断
Deque<Character> deque = new ArrayDeque<>();
for (char c : str.toCharArray()) {
deque.offerLast(c);
}
boolean isPalindrome = true;
while (deque.size() > 1) {
if (deque.pollFirst() != deque.pollLast()) {
isPalindrome = false;
break;
}
}
五、使用注意事项
5.1 禁止存入 null 值
ArrayDeque<String> deque = new ArrayDeque<>();
deque.offerFirst(null); // ❌ 抛出 NullPointerException
deque.offerLast(null); // ❌ 抛出 NullPointerException
原因:
null在内部被用作空槽标记,pollXxx()和peekXxx()在队列为空时返回null- 如果允许存入
null,则无法区分"队列中有 null 元素"和"队列为空" - 源码中所有插入方法均包含
Objects.requireNonNull(e)检查
替代方案:如需存储可能为 null 的元素,使用 LinkedList。
5.2 非线程安全
// ❌ 多线程下不安全
ArrayDeque<Integer> deque = new ArrayDeque<>();
// 线程A: deque.offerFirst(1);
// 线程B: deque.pollLast(); // 可能导致数据竞争
// ✅ 外部同步
Deque<Integer> safeDeque = Collections.synchronizedDeque(new ArrayDeque<>());
// ✅ 或使用并发队列
ConcurrentLinkedDeque<Integer> concurrentDeque = new ConcurrentLinkedDeque<>();
5.3 迭代器的弱一致性
ArrayDeque的迭代器是快速失败(fail-fast)的- 在迭代过程中若通过非迭代器方式修改队列,会抛出
ConcurrentModificationException - 但 fail-fast 行为不保证,不能依赖它来编写程序逻辑
5.4 容量预分配
- 若可预估元素数量,应使用
new ArrayDeque<>(capacity)指定初始容量 - 避免频繁扩容带来的数组复制开销
- 注意:实际容量为
≥ capacity + 1的最小 2 的幂,传入 100 → 实际容量 128
5.5 作为 Queue vs Stack 的方向一致性
| 用途 | 入 | 出 | 方向 |
|---|---|---|---|
| 队列(FIFO) | offerLast / addLast |
pollFirst / removeFirst |
尾进头出 |
| 栈(LIFO) | push = addFirst |
pop = removeFirst |
头进头出 |
注意:
push等价于addFirst而非addLast,栈顶在队首方向。不要混用push/pop与offerLast/pollFirst,否则逻辑混乱。
六、何时选择 ArrayDeque
6.1 选型决策流程
需要双端操作(两端插入/删除)?
├─ 否 → 需要随机访问?
│ ├─ 是 → ArrayList
│ └─ 否 → 需要阻塞等待?
│ ├─ 是 → ArrayBlockingQueue / LinkedBlockingQueue
│ └─ 否 → 单端队列即可 → ArrayDeque(作为 Queue)
└─ 是 → 需要阻塞等待?
├─ 是 → LinkedBlockingDeque
└─ 否 → 需要存 null?
├─ 是 → LinkedList
└─ 否 → ✅ ArrayDeque(首选)
6.2 适合选择 ArrayDeque 的场景
| 场景 | 说明 |
|---|---|
| 单线程栈操作 | push/pop 替代 Stack,性能远超 synchronized 的 Stack |
| 单线程 BFS/层序遍历 | offer/poll 替代 LinkedList 作队列,内存更紧凑 |
| 双端滑动窗口 | 队首出队 + 队尾入队 + 队尾弹出,ArrayDeque 是唯一高效选择 |
| 回文字符串判断 | 首尾同时弹出比较 |
| 撤销/重做(Undo/Redo) | 两个栈(ArrayDeque)分别管理撤销和重做操作 |
| 任务调度(单线程) | 高优先级 addFirst,低优先级 addLast,实现优先调度 |
| 元素数量可预估 | 预分配容量后无扩容开销,内存利用率高 |
6.3 为什么不选阻塞队列(ArrayBlockingQueue / LinkedBlockingQueue)
| 对比项 | ArrayDeque | 阻塞队列(BlockingQueue) |
|---|---|---|
| 阻塞等待 | ❌ 队列空时 poll 直接返回 null |
✅ take() 阻塞等待元素可用 |
| 生产者-消费者 | ❌ 不支持 | ✅ 核心设计目标 |
| 线程安全 | ❌ 非线程安全 | ✅ 内部锁/CS 保证 |
| 单线程性能 | 更优(无锁开销) | 较差(每次操作都加锁) |
| 双端操作 | ✅ 完整 Deque | 仅 LinkedBlockingDeque 支持,且更重 |
核心结论:
- 需要阻塞等待(生产者-消费者模式、线程间协调)→ 选阻塞队列,ArrayDeque 无法满足
- 不需要阻塞等待(纯数据结构使用、单线程算法)→ 选 ArrayDeque,阻塞队列的锁开销是纯粹浪费
典型误用:
// ❌ 单线程 BFS 用了阻塞队列,白白承担锁开销
Queue<Node> queue = new LinkedBlockingQueue<>();
// ✅ 单线程 BFS 应该用 ArrayDeque
Queue<Node> queue = new ArrayDeque<>();
6.4 为什么不选 ArrayList
| 对比项 | ArrayDeque | ArrayList |
|---|---|---|
| 头部插入 | O(1)(head 指针左移) | O(n)(需移动后续所有元素) |
| 头部删除 | O(1)(head 指针右移) | O(n)(需移动后续所有元素) |
| 尾部插入 | O(1)(tail 指针右移) | O(1)(摊销) |
| 尾部删除 | O(1)(tail 指针左移) | O(1) |
| 随机访问 | O(1)(需计算 (head+i) & mask) |
O(1)(直接下标访问,更快) |
| 中间插入/删除 | ❌ 不支持 | O(n) |
| null 元素 | ❌ 禁止 | ✅ 允许 |
| 语义 | 双端队列 / 栈 | 动态数组 / 列表 |
核心结论:
- 需要频繁在头部插入/删除 → 选 ArrayDeque,ArrayList 头部操作 O(n) 是致命瓶颈
- 需要随机访问、中间操作 → 选 ArrayList,ArrayDeque 无
add(int, E)等中间操作方法 - 需要存 null → 选 ArrayList,ArrayDeque 禁止 null
- 纯尾部操作(栈/队列) → 两者尾部都是 O(1),但 ArrayDeque 语义更清晰,且有
push/pop/offer/poll等专用方法
典型误用:
// ❌ 用 ArrayList 模拟队列,头部删除 O(n)
List<Task> queue = new ArrayList<>();
queue.add(task); // 入队(尾部 O(1))
Task t = queue.remove(0); // 出队(头部 O(n)!每次删除后所有元素前移)
// ✅ 用 ArrayDeque,头部删除 O(1)
Deque<Task> queue = new ArrayDeque<>();
queue.offerLast(task); // 入队 O(1)
Task t = queue.pollFirst(); // 出队 O(1)
6.5 最终选型速查表
| 需求 | 首选 | 原因 |
|---|---|---|
| 单线程栈(LIFO) | ArrayDeque | 比 Stack 快,比 LinkedList 省内存 |
| 单线程队列(FIFO) | ArrayDeque | 比 LinkedList 省内存,缓存更友好 |
| 双端操作 | ArrayDeque | 循环数组两端 O(1) |
| 生产者-消费者(线程间通信) | ArrayBlockingQueue | ArrayDeque 非线程安全且无阻塞 |
| 阻塞双端队列 | LinkedBlockingDeque | ArrayDeque 无阻塞能力 |
| 频繁随机访问 + 尾部增删 | ArrayList | ArrayDeque 无真正的 get(i) 语义 |
| 中间插入/删除 | LinkedList | ArrayDeque 不支持中间操作 |
| 需要存 null | LinkedList | ArrayDeque 禁止 null |
| 并发安全队列 | ConcurrentLinkedQueue | ArrayDeque 非线程安全 |
个人总结
项目里为什么选择使用ArrayDeque
- 因为要实现FIFO的队列,需要频繁在头部插入/删除 → 选 ArrayDeque,ArrayList 头部操作 O(n) 是致命瓶颈
- 不需要阻塞等待(纯数据结构使用、单线程算法)→ 选 ArrayDeque,阻塞队列的锁开销是纯粹浪费
取数据,head移动1,放东西,tail移动1
核心就两件事:
- poll:取 elements[head],head 右移 1
- offer:放 elements[tail],tail 右移 1
- 唯一的额外点:到数组边界后绕回来(用位运算 (index+1) & (len-1) 实现循环),而不是扩容。
一个指针吃,一个指针吐,中间就是队列内容。
浙公网安备 33010602011771号