JDK 8 中的 ArrayDeque
是基于动态数组实现的双端队列(Deque
),它支持高效的两端插入/删除操作(时间复杂度为 O(1)
平均时间),同时也可作为栈(Stack
)或队列(Queue
)使用。与 LinkedList
(基于双向链表)相比,ArrayDeque
利用数组的连续内存特性,具有更好的缓存局部性,通常性能更优。
一、ArrayDeque 的核心特性
特性 | 说明 |
---|---|
双端队列 | 支持在队列头部(head )和尾部(tail )高效插入/删除元素。 |
动态数组 | 底层使用可扩容的数组存储元素,自动调整容量以适应数据增长。 |
不允许 null 元素 | 插入 null 会抛出 NullPointerException (与 LinkedList 不同)。 |
非线程安全 | 多线程环境下需手动同步(如使用 Collections.synchronizedDeque 包装)。 |
高效迭代 | 迭代器按数组顺序遍历,支持快速访问(时间复杂度 O(1) 每次访问)。 |
二、ArrayDeque 的核心结构
1. 继承与接口
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, java.io.Serializable
Deque
:双端队列接口,扩展了Queue
,支持addFirst
、removeLast
等操作。AbstractCollection
:提供Collection
接口的骨架实现(如size()
、isEmpty()
等方法)。
2. 核心属性
ArrayDeque
的底层通过动态数组实现,核心属性如下:
// 存储元素的数组(初始容量为 16,或通过构造方法指定)
transient Object[] elements;
// 头部元素的索引(指向第一个有效元素)
transient int head;
// 尾部元素的索引(指向最后一个有效元素的下一个位置)
transient int tail;
// 最小初始容量(用于验证构造方法的参数合法性)
private static final int MIN_INITIAL_CAPACITY = 8;
3. 关键设计说明
- 数组的动态扩容:当数组满时(
(tail + elements.length) & (elements.length - 1) == head
),触发扩容(新容量为原容量的 2 倍)。 - 环形数组结构:通过取模运算(
(index + 1) & (elements.length - 1)
)实现头尾指针的循环,避免频繁移动元素。 - 头尾指针的语义:
head
:指向第一个有效元素的索引(若队列非空)。tail
:指向最后一个有效元素的下一个位置(若队列非空,tail
可能大于head
;若队列为空,head == tail
)。
三、构造方法
ArrayDeque
提供了多种构造方法,核心是初始化底层数组和头尾指针:
1. 默认构造方法(初始容量 16)
public ArrayDeque() {
elements = new Object[16]; // 默认初始容量 16
}
2. 指定初始容量的构造方法
public ArrayDeque(int numElements) {
allocateElements(numElements); // 分配足够容纳 numElements 的数组
}
private void allocateElements(int numElements) {
// 计算最小需要的容量(至少为 numElements,且为 2 的幂次)
int initialCapacity = MIN_INITIAL_CAPACITY;
if (numElements >= initialCapacity) {
initialCapacity = numElements;
// 确保容量为 2 的幂次(通过位运算优化)
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity = (initialCapacity < 0) ? 1 : (initialCapacity >= MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : initialCapacity + 1;
}
elements = new Object[initialCapacity];
}
- 容量优化:初始容量会被调整为 2 的幂次(如传入 10,则调整为 16),便于通过位运算快速计算索引。
3. 从集合初始化的构造方法
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size()); // 分配足够容纳所有元素的数组
addAll(c); // 添加所有元素
}
四、核心方法解析
ArrayDeque
的核心操作围绕双端插入/删除展开,以下是最常用方法的源码解析:
1. addFirst(E e):向头部插入元素
public void addFirst(E e) {
if (e == null) {
throw new NullPointerException("e == null"); // 不允许 null 元素
}
elements[head = (head - 1) & (elements.length - 1)] = e; // 头指针左移(取模实现环形)
if (head == tail) { // 数组已满,需要扩容
doubleCapacity();
}
}
- 头指针计算:
(head - 1) & (elements.length - 1)
等价于(head - 1) % elements.length
,但位运算更快。 - 扩容触发条件:当
head
移动到tail
位置时(数组已满),调用doubleCapacity()
扩容。
2. addLast(E e):向尾部插入元素
public void addLast(E e) {
if (e == null) {
throw new NullPointerException("e == null"); // 不允许 null 元素
}
elements[tail] = e; // 尾指针当前位置存储元素
tail = (tail + 1) & (elements.length - 1); // 尾指针右移(取模实现环形)
if (head == tail) { // 数组已满,需要扩容
doubleCapacity();
}
}
- 尾指针计算:
(tail + 1) & (elements.length - 1)
等价于(tail + 1) % elements.length
。
3. removeFirst():删除并返回头部元素
public E removeFirst() {
E x = pollFirst();
if (x == null) {
throw new NoSuchElementException("Deque is empty");
}
return x;
}
public E pollFirst() {
int h = head;
E[x = elements[h]]; // 获取头部元素
if (x != null) {
elements[h] = null; // 清除引用(帮助 GC)
head = (h + 1) & (elements.length - 1); // 头指针右移
}
return x;
}
- 清除引用:删除元素后将数组位置置为
null
,避免内存泄漏。 - 空队列判断:若
pollFirst()
返回null
,说明队列为空。
4. removeLast():删除并返回尾部元素
public E removeLast() {
E x = pollLast();
if (x == null) {
throw new NoSuchElementException("Deque is empty");
}
return x;
}
public E pollLast() {
int t = (tail - 1) & (elements.length - 1); // 尾指针左移
E x = elements[t];
if (x != null) {
elements[t] = null; // 清除引用
tail = t; // 更新尾指针
}
return x;
}
5. peekFirst() 和 peekLast():查看头部/尾部元素(不删除)
public E peekFirst() {
return (head == tail) ? null : elements[head];
}
public E peekLast() {
return (head == tail) ? null : elements[(tail - 1) & (elements.length - 1)];
}
五、扩容机制(doubleCapacity)
当数组满时((tail + 1) & (elements.length - 1) == head
),触发扩容:
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // 从 head 到数组末尾的元素数量
int newCapacity = n << 1; // 新容量为原容量的 2 倍
if (newCapacity < 0) {
throw new IllegalStateException("Sorry, deque too big");
}
Object[] a = new Object[newCapacity];
// 将原数组从 head 到末尾的元素复制到新数组的起始位置
System.arraycopy(elements, p, a, 0, r);
// 将原数组从 0 到 head-1 的元素复制到新数组的 r 位置之后
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0; // 新头指针指向数组起始位置
tail = n; // 新尾指针指向原数组末尾的下一个位置(即新数组的 r + p = n + p = n + n = 2n,但原 n 是原长度,新长度是 2n,所以 tail = 原长度)
}
- 复制逻辑:将原数组分为两部分(
[head, 末尾]
和[0, head-1]
),合并到新数组中,保持元素顺序。 - 时间复杂度:扩容的时间复杂度为
O(n)
(n
为原数组长度),但均摊到每次插入操作为O(1)
。
六、迭代器实现(Itr)
ArrayDeque
的迭代器按数组顺序遍历元素,支持 hasNext()
、next()
等方法:
private class Itr implements Iterator<E> {
private int cursor; // 下一个要访问的元素索引
private int lastRet; // 上一次访问的元素索引(-1 表示未访问)
public boolean hasNext() {
return cursor != tail; // 游标未到尾部即有元素
}
public E next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
E x = elements[cursor];
lastRet = cursor;
cursor = (cursor + 1) & (elements.length - 1); // 游标右移(环形)
return x;
}
}
- 遍历顺序:迭代器按数组的物理顺序遍历(从
head
到tail
),与双端队列的逻辑顺序一致。
七、ArrayDeque 与 LinkedList 的对比
特性 | ArrayDeque | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
插入/删除(两端) | O(1) 平均时间(无需移动元素) | O(1) 时间(需调整指针) |
随机访问 | O(1) (通过索引) | O(n) (需遍历链表) |
内存占用 | 连续内存,缓存友好 | 分散内存,缓存不友好 |
允许 null | 不允许 | 允许 |
适用场景 | 高频双端操作、需要随机访问 | 高频中间插入/删除、需要双向链表特性 |
八、注意事项
null
元素限制:ArrayDeque
不允许插入null
元素(addFirst(null)
或addLast(null)
会抛出NullPointerException
)。- 线程不安全:多线程环境下同时修改
ArrayDeque
可能导致数据不一致(如ConcurrentModificationException
)。建议使用Collections.synchronizedDeque(new ArrayDeque<>())
包装。 - 作为栈使用:
ArrayDeque
可替代Stack
类(Stack
基于Vector
,线程安全但性能差),推荐使用addFirst()
/removeFirst()
模拟栈的push
/pop
。
九、总结
ArrayDeque
是基于动态数组的高效双端队列,核心优势是两端操作的 O(1)
时间复杂度和数组的缓存友好性。其内部通过环形数组结构和动态扩容机制,兼顾了空间利用率和操作效率。实际开发中,若需要高频双端插入/删除或作为栈/队列使用,ArrayDeque
是优于 LinkedList
的选择。