【Java源码阅读系列15】深度解读Java ArrayDeque 源码

Java ArrayDeque 是基于动态循环数组的双端队列(Deque)实现,支持在队列头部和尾部进行高效的插入、删除操作。它不仅可以作为普通队列(FIFO)使用,还能作为栈(LIFO)使用,且在多数场景下性能优于 LinkedList。本文将从核心结构、关键操作、设计模式等维度,深入解析其源码实现。

一、类定义与核心属性

1.1 类继承关系

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable
  • ArrayDeque 继承自 AbstractCollection(提供集合基础操作),实现了 Deque 接口(双端队列规范),支持克隆和序列化。

1.2 核心属性

transient Object[] elements;  // 存储元素的数组(长度始终为 2 的幂次)
transient int head;           // 队列头部索引(下一个弹出元素的位置)
transient int tail;           // 队列尾部索引(下一个插入元素的位置)
private static final int MIN_INITIAL_CAPACITY = 8;  // 最小初始容量
  • 循环数组:通过 headtail 指针实现“循环”效果。当 headtail 到达数组末尾时,通过位运算 & (elements.length - 1) 回绕到数组头部(例如,数组长度为 16 时,15 + 1 & 15 结果为 0)。
  • 容量特性:数组长度始终是 2 的幂次(如 8、16、32…),这是为了优化索引计算(位运算替代取模)。

二、循环数组的核心操作

2.1 插入元素:addFirst(E e)addLast(E e)

插入操作支持在队列头部(addFirst)或尾部(addLast)插入元素,核心逻辑是调整 head tail 指针,并在数组满时扩容。

addFirst 源码

public void addFirst(E e) {
    if (e == null) throw new NullPointerException();  // 不允许 null 元素
    elements[head = (head - 1) & (elements.length - 1)] = e;  // 头部前一个位置插入
    if (head == tail) doubleCapacity();  // 数组满,扩容
}
  • 索引计算head = (head - 1) & (elements.length - 1) 等价于 (head - 1) % elements.length,但位运算更高效。例如,若 head=0,数组长度为 16,则新 head 为 15((0-1) & 15 = 15)。
  • 扩容触发:当 headtail 重合时(数组满),调用 doubleCapacity() 扩容。

addLast 源码

public void addLast(E e) {
    if (e == null) throw new NullPointerException();
    elements[tail] = e;  // 尾部位置插入
    if ((tail = (tail + 1) & (elements.length - 1)) == head)  // 尾部后移,检查是否满
        doubleCapacity();
}
  • 索引计算tail = (tail + 1) & (elements.length - 1) 实现尾部指针循环后移。

2.2 删除元素:pollFirst()pollLast()

删除操作从头部或尾部弹出元素,核心是获取并清空对应位置的元素,调整指针。

pollFirst 源码

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked") E result = (E) elements[h];
    if (result != null) {  // 队列非空
        elements[h] = null;  // 清空头部位置
        head = (h + 1) & (elements.length - 1);  // 头部后移
    }
    return result;
}

pollLast 源码

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);  // 尾部前一个位置
    @SuppressWarnings("unchecked") E result = (E) elements[t];
    if (result != null) {
        elements[t] = null;  // 清空尾部位置
        tail = t;  // 尾部前移
    }
    return result;
}

三、扩容机制:doubleCapacity()

当数组满(head == tail)时,触发扩容,容量翻倍。

源码实现

private void doubleCapacity() {
    assert head == tail;  // 仅在数组满时调用
    int n = elements.length;
    int newCapacity = n << 1;  // 容量翻倍(2 的幂次特性保持)
    if (newCapacity < 0) throw new IllegalStateException("Deque too big");
    Object[] a = new Object[newCapacity];
    // 迁移元素:原数组从 head 到末尾的部分,复制到新数组头部;原数组头部到 tail 的部分,复制到新数组后续位置
    System.arraycopy(elements, head, a, 0, n - head);
    System.arraycopy(elements, 0, a, n - head, head);
    elements = a;
    head = 0;  // 新数组头部重置为 0
    tail = n;  // 新数组尾部为原容量位置(即新容量的一半)
}
  • 元素迁移:原数组被拆分为两部分([head, 原数组末尾][0, head-1]),复制到新数组的连续位置,保持循环特性。

四、初始化与容量计算

构造函数支持指定初始容量或从集合初始化,核心通过 allocateElements calculateSize 确保数组长度为 2 的幂次。

calculateSize 源码

private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= initialCapacity >>> 1;  // 填充低位为 1
        initialCapacity |= initialCapacity >>> 2;
        initialCapacity |= initialCapacity >>> 4;
        initialCapacity |= initialCapacity >>> 8;
        initialCapacity |= initialCapacity >>> 16;
        initialCapacity++;  // 得到下一个 2 的幂次
        if (initialCapacity < 0) initialCapacity >>>= 1;  // 处理溢出
    }
    return initialCapacity;
}
  • 位运算填充:通过右移和按位或操作,将 numElements 转换为大于等于其值的最小 2 的幂次(例如,numElements=10 会转换为 16)。

五、设计模式应用:迭代器模式

ArrayDeque 通过内部类 DeqIterator(正向迭代器)和 DescendingIterator(反向迭代器)实现 Iterator 接口,符合迭代器模式的设计思想:将遍历逻辑封装在迭代器中,客户端无需关心集合内部结构。

5.1 正向迭代器 DeqIterator

private class DeqIterator implements Iterator<E> {
    private int cursor = head;  // 当前遍历位置(初始为 head)
    private int fence = tail;   // 遍历边界(初始为 tail)
    private int lastRet = -1;   // 最后一次访问的索引

    public boolean hasNext() { return cursor != fence; }

    public E next() {
        if (cursor == fence) throw new NoSuchElementException();
        E result = (E) elements[cursor];
        if (tail != fence || result == null)  // 快速失败检查
            throw new ConcurrentModificationException();
        lastRet = cursor;
        cursor = (cursor + 1) & (elements.length - 1);  // 循环后移
        return result;
    }
}
  • 快速失败Fail-Fast):通过比较 tail 和初始 fence(构造时的 tail),检测集合是否被修改(如插入/删除元素),若不一致则抛出 ConcurrentModificationException

5.2 反向迭代器 DescendingIterator

反向迭代器从 tail 向前遍历到 head,逻辑与正向迭代器类似,但索引计算方向相反:

private class DescendingIterator implements Iterator<E> {
    private int cursor = tail;  // 当前遍历位置(初始为 tail)
    private int fence = head;   // 遍历边界(初始为 head)

    public E next() {
        cursor = (cursor - 1) & (elements.length - 1);  // 循环前移
        E result = (E) elements[cursor];
        if (head != fence || result == null)  // 快速失败检查
            throw new ConcurrentModificationException();
        return result;
    }
}

六、关键特性总结

  • 高效的双端操作:插入、删除的时间复杂度为 O(1)(均摊),优于 LinkedList 的指针操作。
  • 循环数组设计:通过位运算实现索引循环,避免数组复制的高开销(仅在扩容时复制)。
  • 容量优化:数组长度始终为 2 的幂次,位运算替代取模,提升索引计算效率。
  • 线程不安全:未实现同步,多线程环境需外部加锁或使用 ConcurrentLinkedDeque
  • 不允许 null 元素:插入 null 会直接抛出 NullPointerException(与 LinkedList 不同)。

总结

ArrayDeque 是 Java 集合框架中高效的双端队列实现,核心通过循环数组和位运算优化实现了 O(1) 时间的双端操作。其迭代器模式的应用,将遍历逻辑封装在独立类中,保证了集合结构的封装性。在需要双端队列的场景(如 BFS 遍历、任务队列),ArrayDeque 是优于 LinkedList 的选择,但需注意其非线程安全的特性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值