导读:本期聚焦于小伙伴创作的《ArrayDeque 基于循环数组是怎么实现高性能栈与队列的?》,敬请观看详情。把栈和队列都塞进同一个数据结构里,还要避免链表节点的额外开销,JDK里的ArrayDeque给出了一种很直接的答案:用一块连续的内存配合头尾指针做循环。它的底层是一个普通对象数组,head指向队首元素,tail指向下一个可插入位置,当任一端到达数组边界时就绕回下标零。相比LinkedList,它少了节点对象和指针维护,缓存命中率也更高,在频繁压入弹出时表现更稳定。理解扩容触发条件和取模运算的位运算优化,是看懂它性能来源的关键。

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

ArrayDeque 基于循环数组是怎么实现高性能栈与队列的?

一、循环数组的底层结构

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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。