网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

Java ArrayDeque ​

字数: 0 字 时长: 0 分钟

1. ArrayDeque 概述 ​

Java 中有一个叫做:

java
Stack

的类,却没有一个叫做:

java
Queue

的类,因为 Queue 本身是一个接口:

java
public interface Queue<E>
        extends Collection<E> {
}

当需要使用栈时,Java 已经不推荐继续使用 Stack,而是更推荐使用:

java
ArrayDeque

也就是:

text
双端队列

原因前面在 Stack 中已经提到过。

Stack 是一个比较早期的类,它继承自 Vector,核心操作中使用了:

java
synchronized

来保证线程安全。

当我们本身不需要线程安全,例如单线程环境时,这些同步操作会带来额外开销。

image-20250305153831591

所以,当需要使用栈时,可以优先考虑:

text
ArrayDeque

1.1 ArrayDeque 作为栈 ​

java
public static void main(
        String[] args) {

    // 声明一个双端队列
    ArrayDeque<String> stack =
            new ArrayDeque<>();

    // 压入元素
    stack.push(
            "张无忌"
    );

    stack.push(
            "周芷若"
    );

    stack.push(
            "赵敏"
    );

    // 获取栈顶元素
    String top =
            stack.peek();

    System.out.println(
            "栈顶元素为:"
                    + top
    );

    // 弹出栈顶元素
    String pop =
            stack.pop();

    System.out.println(
            "弹出的元素为:"
                    + pop
    );

    // 修改栈顶元素
    stack.pop();

    stack.push(
            "小明"
    );

    System.out.println(
            "修改后的栈为:"
                    + stack
    );

    // 遍历并查找元素
    Iterator<String> iterator =
            stack.iterator();

    int index =
            -1;

    String target =
            "张无忌";

    while (
            iterator.hasNext()
    ) {

        String element =
                iterator.next();

        index++;

        if (
                element.equals(
                        target
                )
        ) {

            break;
        }
    }

    if (index == -1) {

        System.out.println(
                "元素 "
                        + target
                        + " 不存在于队列中"
        );

    } else {

        System.out.println(
                "元素 "
                        + target
                        + " 在队列中的位置为:"
                        + index
        );
    }
}

输出:

text
栈顶元素为:赵敏

弹出的元素为:赵敏

修改后的栈为:[小明, 张无忌]

元素 张无忌 在队列中的位置为:1

在上面的示例中:

text
push()
    ↓
向栈中压入元素


peek()
    ↓
获取栈顶元素


pop()
    ↓
弹出栈顶元素

修改栈顶元素时,可以:

text
pop()
    ↓
删除原栈顶元素
    ↓
push()
    ↓
压入新的栈顶元素

查找元素时,则通过:

java
Iterator

进行遍历。

1.2 ArrayDeque 作为队列 ​

ArrayDeque 实现了:

java
Deque

而:

java
Deque

又继承了:

java
Queue

ArrayDeque 的定义:

java
public class ArrayDeque<E>
        extends AbstractCollection<E>
        implements Deque<E>,
                   Cloneable,
                   Serializable {
}

所以,当需要使用队列时,也可以使用:

java
ArrayDeque

例如:

java
public static void main(
        String[] args) {

    ArrayDeque<String> queue =
            new ArrayDeque<>();

    // 添加元素
    queue.offer(
            "赵敏"
    );

    queue.offer(
            "小昭"
    );

    queue.offer(
            "周芷若"
    );

    // 获取队首元素
    String front =
            queue.peek();

    System.out.println(
            "队首元素为:"
                    + front
    );

    // 弹出队首元素
    String poll =
            queue.poll();

    System.out.println(
            "弹出的元素为:"
                    + poll
    );

    // 修改队列中的元素
    queue.poll();

    queue.offer(
            "小明"
    );

    System.out.println(
            "修改后的队列为:"
                    + queue
    );

    // 查找元素
    Iterator<String> iterator =
            queue.iterator();

    int index =
            0;

    while (
            iterator.hasNext()
    ) {

        String element =
                iterator.next();

        if (
                element.equals(
                        "周芷若"
                )
        ) {

            System.out.println(
                    "元素在队列中的位置为:"
                            + index
            );

            break;
        }

        index++;
    }
}

输出:

text
队首元素为:赵敏

弹出的元素为:赵敏

修改后的队列为:[周芷若, 小明]

元素在队列中的位置为:0

在上面的示例中:

text
offer()
    ↓
向队列中添加元素


peek()
    ↓
获取队首元素


poll()
    ↓
删除并返回队首元素

修改元素时:

text
poll()
    ↓
删除原来的元素
    ↓
offer()
    ↓
添加新的元素

查找元素时,同样可以使用:

java
Iterator

进行遍历。

1.3 LinkedList 作为队列 ​

前面讲过,LinkedList 不只是一个:

java
List

它还实现了:

java
Deque

源码:

java
public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>,
                   Deque<E>,
                   Cloneable,
                   Serializable {
}

所以,当需要使用队列时,同样可以选择:

java
LinkedList

例如:

java
public static void main(
        String[] args) {

    // 创建 LinkedList
    LinkedList<String> queue =
            new LinkedList<>();

    // 添加元素
    queue.offer(
            "小昭"
    );

    queue.offer(
            "赵敏"
    );

    queue.offer(
            "周芷若"
    );

    System.out.println(
            queue
    );

    // 删除元素
    queue.poll();

    System.out.println(
            queue
    );

    // 修改元素
    queue.poll();

    queue.offer(
            "张三丰"
    );

    System.out.println(
            queue
    );

    // 根据下标查找
    System.out.println(
            queue.get(0)
    );

    System.out.println(
            queue.contains(
                    "张无忌"
            )
    );

    // 使用迭代器查找元素
    Iterator<String> iterator =
            queue.iterator();

    while (
            iterator.hasNext()
    ) {

        String element =
                iterator.next();

        if (
                element.equals(
                        "周芷若"
                )
        ) {

            System.out.println(
                    "找到了:"
                            + element
            );

            break;
        }
    }
}

在使用 LinkedList 作为队列时:

text
offer()
    ↓
向队列尾部添加元素


poll()
    ↓
从队列头部删除元素

也可以使用:

text
Iterator

poll()

依次遍历元素。

2. 栈和队列 ​

要理解栈和队列,首先需要理解:

java
Deque

Deque 的意思是:

text
Double Ended Queue
双端队列

它既可以作为:

text
栈

使用,也可以作为:

text
队列

使用。

2.1 Deque 和 Queue ​

Deque 与 Queue 对应的方法:

Queue 方法Deque 方法说明
add(e)addLast(e)向队尾插入元素,失败时抛出异常
offer(e)offerLast(e)向队尾插入元素,失败时返回 false
remove()removeFirst()获取并删除队首元素,失败时抛出异常
poll()pollFirst()获取并删除队首元素,失败时返回 null
element()getFirst()获取但不删除队首元素,失败时抛出异常
peek()peekFirst()获取但不删除队首元素,失败时返回 null

2.2 Deque 和 Stack ​

Deque 与 Stack 对应的方法:

Stack 方法Deque 方法说明
push(e)addFirst(e)向栈顶插入元素,失败时抛出异常
无offerFirst(e)向栈顶插入元素,失败时返回 false
pop()removeFirst()获取并删除栈顶元素,失败时抛出异常
无pollFirst()获取并删除栈顶元素,失败时返回 null
peek()getFirst()获取但不删除栈顶元素,失败时抛出异常
无peekFirst()获取但不删除栈顶元素,失败时返回 null

上面两组一共定义了 Deque 常见的 12 个操作。

对于:

text
添加

删除

获取

通常都有两套接口。

区别主要在于:

一套接口操作失败时抛出异常,另一套接口操作失败时返回特殊值,例如 false 或 null。

除非具体实现本身存在容量限制,否则大多数情况下添加操作并不会失败。

虽然 Deque 的接口不少,但本质上就是:

text
操作容器两端

├── 添加
├── 删除
└── 查看

理解这一点以后,就比较容易掌握。

ArrayDeque 和 LinkedList 都是:

java
Deque

的通用实现。

由于前面已经详细介绍过 LinkedList,这里主要继续分析:

text
ArrayDeque

3. ArrayDeque 底层结构 ​

从名字就能看出来:

text
ArrayDeque
    ↓
Array
    ↓
数组

ArrayDeque 底层通过数组实现。

为了支持:

text
数组两端插入

数组两端删除

这个数组需要按照:

text
循环数组
Circular Array

的方式使用。

也就是说:

数组中的任何位置都有可能成为逻辑上的起点或者终点。

ArrayDeque:

text
非线程安全
Not Thread-Safe

如果多个线程同时使用,需要手动进行同步。

另外:

text
ArrayDeque 不允许保存 null

结构示意图:

image-20250305155130021

其中:

text
head
    ↓
指向首端第一个有效元素


tail
    ↓
指向尾端下一个可以插入元素的空位

因为使用的是循环数组,所以:

text
head 不一定等于 0

tail 也不一定始终大于 head

4. 方法剖析 ​

4.1 addFirst() ​

addFirst(E e) 的作用是:

text
在 Deque 首端插入元素

也就是在:

text
head

之前插入。

如果暂时不考虑:

text
空间不足

数组越界

核心操作可以理解成:

java
elements[--head] =
        e;

示意图:

image-20250305155422042

实际实现需要考虑两个问题:

  1. 空间是否足够。
  2. 下标是否越界。

例如:

text
head = 0

继续执行:

java
addFirst()

如果直接:

text
head - 1

就会得到:

text
-1

发生数组下标越界。

源码通过位运算解决:

java
public void addFirst(
        E e) {

    // 不允许 null
    if (e == null) {

        throw new NullPointerException();
    }

    // 处理循环数组下标
    elements[
            head =
                    (
                            head - 1
                    )
                            & (
                            elements.length - 1
                    )
    ] = e;

    // 空间不足时扩容
    if (head == tail) {

        doubleCapacity();
    }
}

可以看到,空间问题是在插入以后解决的。

因为:

text
tail

始终指向下一个可以插入元素的空位。

所以数组中至少会保留一个:

text
空位

下标越界通过:

java
head =
        (
                head - 1
        )
                & (
                elements.length - 1
        );

解决。

这段代码相当于利用:

text
取模

实现循环下标。

因为:

text
elements.length

是:

text
2 的幂

所以:

text
elements.length - 1

的二进制低位会全部是:

text
1

和:

text
head - 1

进行 & 运算以后,就可以实现类似取模的效果,同时处理:

text
head - 1 = -1

的情况。

4.1.1 doubleCapacity() ​

当数组空间不足时,会调用:

java
doubleCapacity()

进行扩容。

基本思路:

text
创建一个更大的数组
    ↓
容量扩大为原来的两倍
    ↓
复制原数组元素

扩容过程:

image-20250305155540885

因为 ArrayDeque 使用的是循环数组,所以复制会分成两部分:

text
第一次
    ↓
复制 head 右边的元素


第二次
    ↓
复制 head 左边的元素

源码:

java
private void doubleCapacity() {

    assert head == tail;

    int p =
            head;

    int n =
            elements.length;

    // head 右边元素数量
    int r =
            n - p;

    // 新容量扩大为原来的两倍
    int newCapacity =
            n << 1;

    if (newCapacity < 0) {

        throw new IllegalStateException(
                "Sorry, deque too big"
        );
    }

    Object[] newArray =
            new Object[
                    newCapacity
            ];

    // 复制 head 右侧元素
    System.arraycopy(
            elements,
            p,
            newArray,
            0,
            r
    );

    // 复制 head 左侧元素
    System.arraycopy(
            elements,
            0,
            newArray,
            r,
            p
    );

    elements =
            (E[]) newArray;

    head =
            0;

    tail =
            n;
}

首先:

text
head

tail

处于需要扩容的状态。

然后:

java
int r =
        n - p;

计算:

text
head 右侧元素数量

新的容量:

java
int newCapacity =
        n << 1;

也就是:

text
原容量 × 2

然后创建:

java
Object[] newArray

第一次:

java
System.arraycopy()

复制:

text
head 右侧部分

第二次复制:

text
head 左侧部分

最终:

text
elements
    ↓
指向新数组


head
    ↓
0


tail
    ↓
原数组长度 n

原来的 elements 数组失去引用以后,就可以由垃圾回收器进行回收。

4.2 addLast() ​

addLast(E e) 的作用:

text
在 Deque 尾端插入元素

也就是:

text
tail

指向的位置。

由于 tail 始终指向:

text
下一个可以插入的空位

所以核心操作:

java
elements[tail] =
        e;

插入完成以后:

text
tail
    ↓
向后移动

如果空间已经耗尽:

text
doubleCapacity()

进行扩容。

源码:

java
public void addLast(
        E e) {

    // 不允许 null
    if (e == null) {

        throw new NullPointerException();
    }

    // 写入 tail
    elements[tail] =
            e;

    // tail 向后移动
    if (
            (
                    tail =
                            (
                                    tail + 1
                            )
                                    & (
                                    elements.length - 1
                            )
            ) == head
    ) {

        doubleCapacity();
    }
}

下标循环处理方式与:

java
addFirst()

相同。

4.3 pollFirst() ​

pollFirst() 的作用:

text
删除并返回 Deque 首端元素

也就是:

text
head

位置上的元素。

如果容器不为空:

text
返回 elements[head]

并让:

text
head

向后移动。

由于:

text
ArrayDeque 不允许保存 null

所以:

java
elements[head] == null

就意味着:

text
Deque 为空

源码:

java
public E pollFirst() {

    E result =
            elements[head];

    // null 表示队列为空
    if (result == null) {

        return null;
    }

    // 清除原来的引用
    elements[head] =
            null;

    // head 向后移动
    head =
            (
                    head + 1
            )
                    & (
                    elements.length - 1
            );

    return result;
}

整个过程:

text
读取 head 元素
    ↓
为空则返回 null
    ↓
原位置置为 null
    ↓
head 向后移动
    ↓
返回原元素

4.4 pollLast() ​

pollLast() 的作用:

text
删除并返回 Deque 尾端元素

真正的最后一个元素位于:

text
tail 的前一个位置

所以首先计算:

java
int t =
        (
                tail - 1
        )
                & (
                elements.length - 1
        );

源码:

java
public E pollLast() {

    // tail 前一个位置才是最后一个元素
    int t =
            (
                    tail - 1
            )
                    & (
                    elements.length - 1
            );

    E result =
            elements[t];

    // null 表示队列为空
    if (result == null) {

        return null;
    }

    // 清除引用
    elements[t] =
            null;

    tail =
            t;

    return result;
}

整个过程:

text
找到 tail 前一个位置
    ↓
读取元素
    ↓
为空则返回 null
    ↓
原位置置为 null
    ↓
tail 前移
    ↓
返回元素

4.5 peekFirst() ​

peekFirst() 的作用:

text
返回但不删除 Deque 首端元素

也就是:

text
head

位置上的元素。

源码:

java
public E peekFirst() {

    return elements[head];
}

如果 Deque 为空:

text
elements[head]
    ↓
null

所以直接返回 null。

4.6 peekLast() ​

peekLast() 的作用:

text
返回但不删除 Deque 尾端元素

也就是:

text
tail 前一个位置

源码:

java
public E peekLast() {

    return elements[
            (
                    tail - 1
            )
                    & (
                    elements.length - 1
            )
    ];
}

5. 小结 ​

当需要实现:

text
FIFO
先进先出

或者:

text
LIFO
后进先出

的数据结构时,都可以考虑使用:

java
ArrayDeque

一些常见场景:

  • 管理任务队列:使用 ArrayDeque 保存任务元素,从一端添加任务,再从另一端取出任务,可以按照先进先出的顺序进行处理。
  • 实现栈:ArrayDeque 支持 push()、pop()、peek() 等操作,可以用于后进先出的场景。
  • 实现缓存:当缓存数量超过限制时,可以从队列一端删除旧数据,从另一端添加新数据。
  • 实现事件处理器:可以从队列头部取出事件进行处理,再从尾部添加新的事件。

ArrayDeque 是 Java 标准库中的一种:

text
双端队列

底层基于:

text
数组

实现。

与 LinkedList 相比:

text
ArrayDeque
    ↓
底层数组


LinkedList
    ↓
底层链表

数组使用连续的存储空间保存元素引用,可以更好地利用 CPU 缓存。

而 LinkedList 由多个节点组成,每一个节点都需要保存:

text
数据

前后节点引用

在使用过程中也会涉及节点对象的创建。

现代 CPU 对数据局部性比较敏感,如果需要访问的数据在内存中比较连续,就可以更好地利用 CPU Cache。

当然,ArrayDeque 同样需要考虑:

text
数组扩容

带来的成本。

当元素数量达到当前数组容量限制时:

text
创建新数组
    ↓
容量扩大
    ↓
复制旧数组数据

数组复制的时间复杂度:

text
O(n)

不过,通过扩容策略可以减少数组频繁复制带来的消耗。

ArrayDeque 不仅支持普通队列操作:

text
添加元素

删除元素

获取队首元素

获取队尾元素

同时也支持栈操作:

text
push()

pop()

peek()

因此:

ArrayDeque 既可以作为双端队列使用,也可以作为普通队列或者栈使用。

Noctiflorous Garden · MIT License