AI生成-Java ArrayDeque 技术文档

Java ArrayDeque 技术文档

一、概述

ArrayDeque(Array Double Ended Queue)是 Java 集合框架中基于动态循环数组实现的双端队列,同时实现了 DequeQueue 接口。

核心特性

  • 双端操作:队首和队尾均可在 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;           // 队尾下一个插入位置索引

核心思想headtail 在数组中双向增长,到达边界后绕回到数组另一端,形成逻辑上的环形

数组物理视图(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(即队列已满)时触发扩容。

扩容流程

  1. 分配新数组,容量为原来的 2 倍
  2. 将旧数组元素按逻辑顺序复制到新数组(从 head 开始,绕回复制)
  3. 重置 head = 0tail = 旧元素数量
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) 在队首插入元素;队列满时 addFirstIllegalStateExceptionofferFirst 返回 false
删除 removeFirst() pollFirst() 移除并返回队首元素;队列为空时 removeFirstNoSuchElementExceptionpollFirst 返回 null
查看 getFirst() peekFirst() 获取但不移除队首元素;队列为空时 getFirstNoSuchElementExceptionpeekFirst 返回 null

3.2 队尾操作(Last / Tail)

方法 抛异常 返回特殊值 说明
插入 addLast(e) offerLast(e) 在队尾插入元素;队列满时 addLastIllegalStateExceptionofferLast 返回 false
删除 removeLast() pollLast() 移除并返回队尾元素;队列为空时 removeLastNoSuchElementExceptionpollLast 返回 null
查看 getLast() peekLast() 获取但不移除队尾元素;队列为空时 getLastNoSuchElementExceptionpeekLast 返回 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,单线程下性能浪费
  • Stackpush/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/popofferLast/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) 实现循环),而不是扩容。
    一个指针吃,一个指针吐,中间就是队列内容。
posted @ 2026-06-17 14:12  deyang  阅读(3)  评论(0)    收藏  举报