JDK 8 ArrayDeque 详解及详细源码展示

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,支持 addFirstremoveLast 等操作。
  • 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;
    }
}
  • 遍历顺序:迭代器按数组的物理顺序遍历(从 headtail),与双端队列的逻辑顺序一致。

七、ArrayDeque 与 LinkedList 的对比

特性ArrayDequeLinkedList
底层结构动态数组双向链表
插入/删除(两端)O(1) 平均时间(无需移动元素)O(1) 时间(需调整指针)
随机访问O(1)(通过索引)O(n)(需遍历链表)
内存占用连续内存,缓存友好分散内存,缓存不友好
允许 null不允许允许
适用场景高频双端操作、需要随机访问高频中间插入/删除、需要双向链表特性

八、注意事项

  1. null 元素限制ArrayDeque 不允许插入 null 元素(addFirst(null)addLast(null) 会抛出 NullPointerException)。
  2. 线程不安全:多线程环境下同时修改 ArrayDeque 可能导致数据不一致(如 ConcurrentModificationException)。建议使用 Collections.synchronizedDeque(new ArrayDeque<>()) 包装。
  3. 作为栈使用ArrayDeque 可替代 Stack 类(Stack 基于 Vector,线程安全但性能差),推荐使用 addFirst()/removeFirst() 模拟栈的 push/pop

九、总结

ArrayDeque 是基于动态数组的高效双端队列,核心优势是两端操作的 O(1) 时间复杂度数组的缓存友好性。其内部通过环形数组结构和动态扩容机制,兼顾了空间利用率和操作效率。实际开发中,若需要高频双端插入/删除或作为栈/队列使用,ArrayDeque 是优于 LinkedList 的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值