Java Deque 深入解析:现代化的栈与队列解决方案

0 阅读5分钟

1. 什么是 Deque?

Deque(发音通常同 "deck")是 "Double Ended Queue"(双端队列)的缩写。它是Java集合框架中的一个核心接口,定义了一种线性集合,允许在队列的两端进行元素的添加和移除操作

Deque 的这个特性使它成为一个极其灵活的数据结构,可以同时扮演两种重要的角色:

  1. 栈 (Stack):当只在一端(例如,头部)进行添加和移除操作时,Deque 就表现为“后进先出”(LIFO)的栈。它是官方推荐的、用于替代老旧 Stack 类的现代化方案
  2. 队列 (Queue):当在一端添加元素(队尾)、在另一端移除元素(队首)时,Deque 就表现为“先进先出”(FIFO)的队列。

Deque 是一个接口,因此在使用时需要创建它的具体实现类的实例,最常用的是 ArrayDequeLinkedList

2. Deque 的核心优势

  • 一物两用,极其灵活:一个 Deque 对象既能满足栈的需求,也能满足队列的需求,甚至可以完成更复杂的操作(如在队首入队)。
  • 高性能Deque 的主要实现类 ArrayDeque 是非同步的,基于可变数组,其性能远超于基于 Vector 的老旧 Stack 类。对于栈和队列操作,它通常能提供摊销常数时间(amortized constant time)的性能。
  • 设计优良:遵循了Java集合框架“面向接口编程”的最佳实践,将接口定义与具体实现分离,使得代码更加健壮和可维护。

3. Deque 作为栈 (LIFO) 使用

这是替代 Stack 类的标准用法。Deque 接口特意提供了与 Stack 类同名的方法 (push, pop, peek),使得迁移和使用都非常方便。

栈操作 (Stack Operation)Stack 方法Deque 等效方法描述
入栈push(e)push(e)addFirst(e)将元素添加到栈顶(Deque的头部)
出栈pop()pop()removeFirst()移除并返回栈顶元素
查看栈顶peek()peek()getFirst()查看栈顶元素但不移除

代码示例:

import java.util.Deque;
import java.util.ArrayDeque;

public class DequeAsStackDemo {
    public static void main(String[] args) {
        // 推荐使用 ArrayDeque 作为 Deque 的实现来模拟栈
        Deque<String> stack = new ArrayDeque<>();

        // 入栈
        stack.push("Java");
        stack.push("Python");
        stack.push("C++");

        System.out.println("当前栈顶元素: " + stack.peek()); // 输出: C++

        // 出栈
        while (!stack.isEmpty()) {
            System.out.println("出栈: " + stack.pop());
        }
        // 输出顺序: C++, Python, Java
    }
}

4. Deque 作为队列 (FIFO) 使用

Deque 也可以完美地实现一个标准的队列。

队列操作 (Queue Operation)Queue 方法Deque 等效方法描述
入队add(e) / offer(e)addLast(e) / offerLast(e)将元素添加到队尾
出队remove() / poll()removeFirst() / pollFirst()移除并返回队首元素
查看队首element() / peek()getFirst() / peekFirst()查看队首元素但不移除

代码示例:

import java.util.Deque;
import java.util.ArrayDeque;

public class DequeAsQueueDemo {
    public static void main(String[] args) {
        Deque<String> queue = new ArrayDeque<>();

        // 入队 (在尾部添加)
        queue.offerLast("任务A");
        queue.offerLast("任务B");
        queue.offerLast("任务C");

        System.out.println("当前队首任务: " + queue.peekFirst()); // 输出: 任务A

        // 出队 (从头部移除)
        while (!queue.isEmpty()) {
            System.out.println("处理任务: " + queue.pollFirst());
        }
        // 输出顺序: 任务A, 任务B, 任务C
    }
}

5. Deque 接口核心方法详解

Deque 的方法设计有一个重要特点:几乎每个操作都有两种形式。

  1. 抛出异常:如果操作失败(例如,从空队列中移除元素),会抛出 Exception
  2. 返回特殊值:如果操作失败,会返回 nullfalse

在需要严格控制程序流程时,推荐使用返回特殊值的方法。

操作抛出异常的方法返回特殊值的方法描述
在头部插入addFirst(e)offerFirst(e)将元素添加到 Deque 的头部。offerFirst在有容量限制时更安全。
在尾部插入addLast(e)offerLast(e)将元素添加到 Deque 的尾部。
从头部移除removeFirst()pollFirst()移除并返回头部元素。如果队列为空,removeFirst抛异常,pollFirst返回null
从尾部移除removeLast()pollLast()移除并返回尾部元素。如果队列为空,removeLast抛异常,pollLast返回null
获取头部元素getFirst()peekFirst()获取头部元素但不移除。如果队列为空,getFirst抛异常,peekFirst返回null
获取尾部元素getLast()peekLast()获取尾部元素但不移除。如果队列为空,getLast抛异常,peekLast返回null

栈相关方法:

  • push(e): 内部调用 addFirst(e)
  • pop(): 内部调用 removeFirst()
  • peek(): 内部调用 getFirst()

6. Deque 的主要实现类

ArrayDeque

  • 底层结构:可调整大小的数组
  • 特点
    • 不允许 null 元素。
    • 非线程安全。
    • 性能极高,是实现栈和队列的首选
  • 最佳用途:当你需要一个纯粹的、高性能的栈或队列时。

LinkedList

  • 底层结构双向链表
  • 特点
    • 允许 null 元素。
    • 非线程安全。
    • 除了 Deque 接口,它还实现了 List 接口,功能更丰富。
  • 最佳用途:当你在同一个对象上既需要 List 的随机访问/修改功能,又需要 Deque 的两端操作功能时。

怎么选?

  • 通用场景:无脑选择 ArrayDeque
  • 特殊需求:需要存储 null,或者需要频繁地在集合中间进行增删操作时,可以考虑 LinkedList

7. 实际应用场景

Deque 的应用非常广泛,特别是在算法问题中:

  • 括号匹配/表达式求值:经典的栈应用。
  • 树的深度优先搜索 (DFS):可以使用 Deque 作为栈来存储待访问的节点。
  • 树的广度优先搜索 (BFS):可以使用 Deque 作为队列来存储待访问的节点。
  • 滑动窗口问题:例如“滑动窗口最大值”,Deque 可以用来维护一个单调队列,高效地找到当前窗口的最值。
  • 任务调度器:可以利用 Deque 实现一个工作窃取(Work-Stealing)算法,调度器可以从自己队列的头部获取任务,也可以从其他调度器队列的尾部“窃取”任务。

8. 总结

Deque 是Java中一个设计精良、功能强大的接口。它不仅是老旧 Stack 类的完美替代品,还提供了高效的队列实现。

核心要点:

  • Deque 是一个双端队列接口。
  • 它可以作为栈 (LIFO)队列 (FIFO) 使用。
  • ArrayDeque 是首选的实现类,性能高,不允许 null
  • LinkedList 在需要 ListDeque 双重功能时使用。
  • 在新的Java代码中,请始终优先选择 Deque (通常是 ArrayDeque) 来满足你对栈或队列的需求。