ArrayDeque 是 Java 集合框架中一个基于循环数组实现的双端队列,它同时可以作为栈和队列使用。由于其底层使用连续内存数组而非链表节点,在绝大多数增删操作场景下都比 LinkedList 拥有更好的性能和更低的内存占用。本文将从底层结构、核心操作原理、扩容机制以及代码实践几个方面,详细分析它是如何高效支撑栈与队列行为的。

一、循环数组的底层结构
ArrayDeque 内部维护一个对象数组 element,以及两个整型索引 head 和 tail。head 表示当前队列首元素所在的下标,tail 表示下一个可以插入元素的位置。与普通的线性数组不同,这两个指针可以在数组末尾“绕回”到开头,从而形成逻辑上的环形结构。这种设计让队首和队尾的插入、删除都可以在摊还常数时间内完成。
由于数组长度始终保持为 2 的幂次方,ArrayDeque 在计算下一个位置时不使用取模运算,而是用位运算 (i + 1) & (elements.length - 1) 来实现循环。这种方式比取模更快,也避免了负数下标带来的边界处理复杂度。例如当数组长度为 16 时,length - 1 为 15(二进制 1111),任何加一后的下标与 15 做与运算,都会自然落在 0 到 15 之间。
1.1 核心字段定义
在 JDK 源码中,几个关键字段的声明如下。这些字段共同构成了循环数组的基础运行状态,理解它们对后续分析操作逻辑非常重要。
// 存储元素的数组,长度总是 2 的幂 transient Object[] elements; // 队首元素下标 transient int head; // 下一个插入位置的下标 transient int tail; // 最小容量,必须是 2 的幂 private static final int MIN_INITIAL_CAPACITY = 8;
从上面代码可以看出,elements 并不限制存储的具体类型,而是通过泛型擦除在外部表现为 E 类型。head 和 tail 的初始值均为 0,此时队列为空。当 head 等于 tail 时,代表当前没有任何有效元素。
二、作为队列的核心操作
当把 ArrayDeque 当作队列使用时,我们一般调用 offer(或 add)从队尾入队,调用 poll 从队首出队。由于首尾指针独立移动,这两端操作互不干扰,且都不需要移动中间元素,因此效率很高。
队尾插入时,元素被放到 tail 指向的位置,随后 tail 向前推进一位;如果 tail 越界则回到 0。队首删除时,先取出 head 位置的元素,将该位置置为 null 以帮助垃圾回收,然后 head 向后推进。若 head 越界同样回到 0。下面的示例展示了最基本的队列用法。
import java.util.ArrayDeque;
import java.util.Queue;
public class QueueDemo {
public static void main(String[] args) {
Queue<String> queue = new ArrayDeque<>();
// 入队
queue.offer("任务A");
queue.offer("任务B");
// 出队
String first = queue.poll();
System.out.println("处理:" + first);
}
}
2.1 空满判断与容量关系
循环数组有一个经典难题:当 head 等于 tail 时,既可能是空队列,也可能是满队列。ArrayDeque 的解法是永远保留一个空位,即实际元素个数最多为数组长度减一。当插入后发现 tail 等于 head,就触发扩容。这样便能用 head 和 tail 的相对位置唯一确定空满状态,不需要额外字段记录 size(不过 JDK 中其实用继承的 size 字段做了计数,逻辑上仍遵循此约定)。
这种设计虽然浪费了一个数组槽位,但换来了极高的判断效率,也避免了模运算之外的复杂分支。对于栈场景,由于只在一端操作,这个空位规则同样适用,只是 head 和 tail 的变化方向有所不同。
三、作为栈的高性能表现
ArrayDeque 实现了 Deque 接口,因此可以用 push 和 pop 方法模拟栈。与早期推荐的 Stack 类(基于 Vector,方法加锁导致性能差)不同,ArrayDeque 没有同步开销,且基于数组局部性原理,在压栈和弹栈时 CPU 缓存利用率明显更高。
压栈操作本质是在 head 端插入:先将 head 向前移动一位(若越界则到数组末尾),再赋值;弹栈则是取出 head 位置元素并将 head 后移。因为只在数组一端附近修改,几乎没有内存碎片和节点创建销毁成本。下面是一段栈用法的代码。
import java.util.ArrayDeque;
public class StackDemo {
public static void main(String[] args) {
ArrayDeque<Integer> stack = new ArrayDeque<>();
stack.push(10);
stack.push(20);
// 弹出栈顶
int top = stack.pop();
System.out.println("栈顶元素:" + top);
}
}
3.1 与 LinkedList 的对比
LinkedList 实现栈或队列时,每次操作都要新建 Node 对象,维护前驱和后继指针,这不仅增加 GC 压力,还因为节点在堆中分散而降低缓存命中率。ArrayDeque 使用一块连续数组,遍历和相邻访问都更友好。在百万级压测中,ArrayDeque 的 push、pop 通常比 LinkedList 快一倍以上,且内存占用更小。
当然,ArrayDeque 不是线程安全的,如果在并发环境使用需要外部加锁,或改用 ConcurrentLinkedDeque。但对于单线程算法、解析器、调度器内部数据结构而言,它几乎是栈和队列的首选。
四、扩容机制解析
当数组即将填满时,ArrayDeque 会分配一块两倍大小的新数组,并将原数据从 head 开始按顺序拷贝到新数组的前部,随后重置 head 为 0、tail 为原元素个数。由于容量是 2 的幂,扩容后位运算掩码仍然有效,不需要改动其他逻辑。
扩容是一个 O(n) 操作,但摊还到每次插入仅为 O(1)。初始容量若过小,会导致频繁扩容;过大则浪费内存。因此在明确数据规模时,建议通过构造函数指定容量,例如 new ArrayDeque(1024),可显著减少运行期拷贝。
// 指定初始容量,避免频繁扩容
ArrayDeque<String> deque = new ArrayDeque<>(1024);
deque.addFirst("左端");
deque.addLast("右端");
4.1 扩容代码片段理解
源码中双倍扩容的核心思路是:计算新容量,创建新数组,以 head 为起点分段复制。由于旧数组可能在逻辑上被 head 截断成两段(一段是 head 到末尾,一段是开头到 tail),拷贝时要分两次进行。理解这一点,就能明白为什么循环数组在物理上仍是直线,在逻辑上才是环形。
这种分段复制虽然代码略显繁琐,但保证了元素顺序与使用者预期一致,也维持了循环不变量。对普通开发者来说,只需知道扩容自动发生且代价可控即可,不必在业务层手动干预。
五、使用建议与总结
综合来看,ArrayDeque 凭借循环数组、2 的幂容量、位运算循环以及无锁单线程设计,在栈和队列场景中提供了非常优异的性能。它替代了古老的 Stack 类,也普遍优于 LinkedList 的双端队列实现。在实际开发中,若需要 LIFO 栈,应优先使用 ArrayDeque.push 和 pop;若需要 FIFO 队列,使用 offer 和 poll 即可。
需要注意的是,不要在迭代时结构性修改 deque,否则会触发 ConcurrentModificationException;同时,由于它不允许 null 元素,插入前需做好空值校验。掌握这些细节,就能把这块“会转圈”的数组用得既稳又快。
ArrayDeque循环数组双端队列修改时间:2026-08-08 21:36:40