源码 | Queue、Deque、ArrayDeque源码分析

本文深入探讨Java集合框架中ArrayDeque的实现原理,分析其作为队列时为何比LinkedList更高效,包括其基于数组的实现方式、头尾指针操作、扩容机制及与ArrayList的对比。

ArrayDeque,Collection框架中不起眼的一个类有删改

继承关系

8923455-6d815404deb0ef23.png

继承关系

 

类名中的 Array姓氏,它是基于数组实现的。

类注释中,有句话引起了我的注意:

/**
 * This class is likely to be faster than
 * {@link Stack} when used as a stack, and faster than {@link LinkedList}
 * when used as a queue.
 */

(Stack先不管)注释中后半句说,ArrayDeque作为队列时比LinkedList快,看看它是怎么办到的!

属性:

transient Object[] elements;    //基于数组实现
transient int head;    //头指针
transient int tail;    //尾巴指针

构造器

private static final int MIN_INITIAL_CAPACITY = 8; // 默认大小

private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}
// The main insertion and extraction methods are addFirst,
// addLast, pollFirst, pollLast. The other methods are defined in
// terms of these.

方法

以上注释有云,核心方法就4个,我们从add方法入手。

插入

  • addFirst
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;    //关键
    if (head == tail)
        doubleCapacity();
}

head = (head - 1) & (elements.length - 1),玄机就在这里。如果你对1.8的HashMap足够了解,就会知道hashmap的数组大小同样始终是2的次幂。其中很重要的一个原因就是:当lengh是2的次幂的时候,某数字 x 的操作 x & (length - 1) 等价于 x % length,而对二进制的计算机来说 & 操作要比 % 操作效率更好
而且head = (head - 1) & (elements.length - 1),(head初始值0)第一次就将head指针定位到数组末尾了。

画图分析下:

 

8923455-ff7361ac7c6ca16b.png

队列首添加元素

可见,head指针从后向前移动。

addLast

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

8923455-fa73cbf8438c0420.png

队列尾部添加元素

addLast和addFirst原理相同,只是addLast控制tail指针,从前向后移动!

上图中再做一次add操作,指针将会重合。比如,再一次addFirst之后:

 

8923455-96a786e3bed593a4.png

扩容操作

if (head == tail)
        doubleCapacity();    //扩容触发

扩容

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;    //左移,等价乘2,依然保持2的次幂
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

8923455-af13b2b1011657db.png

扩容实现

通过数组拷贝和重新调整指针,完成了扩容。

至于pollFirst、pollLast是addFirst、addLast的相反操作,原理相似,不多做分析。

作为队列时,ArrayDeque效率为什么会比LinkedList更好?

我认为:因为只操作首尾元素,不会像ArrayList那样插入或删除中间元素,不会设计到大量的复制移动操作,并且基于数组。所以效率更高。

<think>嗯,用户让我介绍Java Queue的解析与源码分析,首先我需要确定用户的需求是什么。可能他们正在学习Java集合框架,想深入了解Queue接口及其实现类。用户可能有一定的Java基础,但对队列的具体实现和内部机制不太清楚。我需要从Queue的基本概念讲起,然后逐步深入到常用实现类,比如LinkedList、PriorityQueueArrayDeque,再结合源码分析他们的内部结构和方法实现。 首先,我应该解释Queue接口的定义和核心方法,比如add、offer、remove、poll等,区分它们的区别,比如抛出异常和返回特殊值的不同情况。然后,介绍主要的实现类,每个实现类的特点,比如LinkedList是链表实现,PriorityQueue是基于堆的优先级队列,ArrayDeque是循环数组实现的双端队列。 接下来是源码分析部分,这部分需要详细一些。比如在分析PriorityQueue时,要说明它的内部结构是一个最小堆,默认初始容量,扩容机制等。对于ArrayDeque,需要解释循环数组的工作原理,如何计算头尾指针,以及扩容时的操作。在讲解的时候要注意避免过于深入复杂的细节,保持清晰易懂。 用户可能还关心线程安全的问题,所以需要提到Java中的线程安全队列实现,比如ConcurrentLinkedQueue和BlockingQueue的实现类,但可能不需要深入,因为用户的问题主要集中在Queue本身的解析。不过如果有余力的话,可以简单提一下,作为扩展内容。 另外,用户可能希望了解不同队列的使用场景,比如什么时候用PriorityQueue,什么时候用ArrayDeque,这时候需要结合它们的特性,比如优先级队列适用于任务调度,而ArrayDeque适合作为栈或普通队列使用,因为它的性能较好。 在写作过程中,需要注意结构清晰,分点讲解,避免大段文字。可能还需要比较不同实现之间的性能差异,比如LinkedList和ArrayDeque在频繁插入删除时的效率,这样用户能更好地选择适合自己需求的队列实现。 最后,要确保源码分析部分准确,可能需要参考JDK的源码,比如JDK 11或更高版本的实现,但要注意不同版本之间可能有差异,最好说明是基于哪个版本的分析。同时,可以给出一些示例代码,帮助用户理解如何使用这些队列,以及内部是如何运作的。 总结下来,回答的结构应该是:Queue接口概述、核心方法、常用实现类的特点及源码分析(包括内部结构、关键方法实现、扩容机制等),使用场景和注意事项。这样用户能够全面了解Java Queue的相关知识,并且通过源码分析加深理解。</think>### Java Queue 解析与源码分析 #### 1. Queue 接口概述 Java 中的 `Queue` 接口是 `java.util` 包中定义的先进先出(FIFO)数据结构,属于 Java 集合框架的一部分。它扩展了 `Collection` 接口,主要提供以下核心操作: - **插入**:`add(e)`(失败抛异常)、`offer(e)`(失败返回 `false`) - **移除**:`remove()`(空队列抛异常)、`poll()`(空队列返回 `null`) - **查看队头**:`element()`(空队列抛异常)、`peek()`(空队列返回 `null`) #### 2. 常用实现类 - **LinkedList**:基于双向链表实现,支持队列和双端队列(`Deque`)。 - **PriorityQueue**:基于堆的优先级队列,元素按自然顺序或自定义 `Comparator` 排序。 - **ArrayDeque**:基于循环数组的双端队列,性能优于 `LinkedList`,适合高频操作。 --- ### 源码分析(以 JDK 17 为例) #### 1. PriorityQueue **核心特性**:基于最小堆(默认)的完全二叉树结构,支持动态扩容。 **内部结构**: ```java transient Object[] queue; // 存储元素的数组 private int size = 0; // 当前元素数量 private final Comparator<? super E> comparator; // 比较器 ``` **扩容机制**: ```java private void grow(int minCapacity) { int oldCapacity = queue.length; int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); if (newCapacity > MAX_ARRAY_SIZE) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); } ``` - 小容量时翻倍扩容(如容量 < 64,则每次扩容为 `oldCapacity + (oldCapacity + 2)`)。 - 大容量时按 50% 扩容(`oldCapacity >> 1`)。 **插入操作(`offer(e)`)**: ```java public boolean offer(E e) { if (e == null) throw new NullPointerException(); int i = size; if (i >= queue.length) grow(i + 1); // 扩容 siftUp(i, e); // 堆上浮调整 size = i + 1; return true; } ``` - 插入元素后通过 `siftUp()` 调整堆结构,保持父节点小于子节点。 --- #### 2. ArrayDeque **核心特性**:基于循环数组的高效双端队列,默认初始容量 16。 **内部结构**: ```java transient Object[] elements; // 存储元素的数组 transient int head; // 队头指针 transient int tail; // 队尾指针 ``` **插入元素(`addLast(e)`)**: ```java 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) & (length - 1)` 实现循环(要求容量为 2 的幂)。 - 触发扩容时,容量翻倍(`doubleCapacity()`)。 **扩容方法**: ```java private void doubleCapacity() { int newCapacity = elements.length << 1; // 容量翻倍 Object[] a = new Object[newCapacity]; System.arraycopy(elements, head, a, 0, elements.length - head); System.arraycopy(elements, 0, a, elements.length - head, head); elements = a; head = 0; tail = elements.length; } ``` - 将原数组分为两段复制到新数组,确保循环连续性。 --- #### 3. LinkedList(作为 Queue) **核心特性**:基于双向链表实现,支持快速头尾操作。 **节点结构**: ```java private static class Node<E> { E item; Node<E> next; Node<E> prev; } ``` **入队操作(`offer(e)`)**: ```java public boolean offer(E e) { return add(e); // 调用 addLast() } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; // 空链表初始化 else l.next = newNode; size++; } ``` --- ### 使用场景对比 | **实现类** | **特点** | **适用场景** | |----------------|---------------------------------------|--------------------------------| | `PriorityQueue` | 按优先级排序,插入/删除 O(log n) | 任务调度、贪心算法 | | `ArrayDeque` | 高性能双端队列,内存紧凑 | 高频队列/栈操作、滑动窗口 | | `LinkedList` | 支持随机访问,但内存开销较大 | 需要同时使用队列和列表功能的场景 | --- ### 注意事项 1. **线程不安全**:上述实现均非线程安全,多线程环境下需使用 `ConcurrentLinkedQueue` 或 `BlockingQueue`。 2. **空值限制**:`PriorityQueue` 不允许 `null` 元素,`ArrayDeque` 和 `LinkedList` 允许。 3. **性能选择**:优先选择 `ArrayDeque` 替代 `LinkedList`(除非需要频繁随机访问)。 通过源码分析可以更深入理解各队列的设计取舍,实际开发中应根据需求选择最合适的实现。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值