1. Deque接口概述
双端队列(Deque)是Java集合框架中一个非常重要的接口,它扩展自Queue接口,提供了在队列两端进行操作的完整方法集。与普通队列(Queue)只能在一端插入、另一端删除不同,Deque允许在头部和尾部进行插入、删除和查看操作。
在实际开发中,Deque的应用场景非常广泛:
- 需要实现栈(LIFO)结构时,使用Deque比Stack类更推荐
- 需要实现工作窃取(work-stealing)算法时
- 滑动窗口算法的实现
- 需要频繁在集合两端操作元素的场景
2. Deque核心方法解析
2.1 插入操作方法
Deque提供了两组插入方法,分别对应队列的两端:
// 队首插入 void addFirst(E e); boolean offerFirst(E e); // 队尾插入 void addLast(E e); boolean offerLast(E e);这两组方法的区别在于:
- addXxx方法在容量受限的实现中(如ArrayDeque)可能会抛出IllegalStateException
- offerXxx方法则通过返回boolean值表示操作是否成功
最佳实践:在不确定容量限制的情况下,优先使用offerXxx方法
2.2 移除操作方法
同样地,移除操作也分为队首和队尾两组:
// 队首移除 E removeFirst(); E pollFirst(); // 队尾移除 E removeLast(); E pollLast();关键区别:
- removeXxx方法在队列为空时会抛出NoSuchElementException
- pollXxx方法在队列为空时返回null
2.3 查看操作方法
查看方法不会移除元素,只是获取队列两端的元素:
// 查看队首 E getFirst(); E peekFirst(); // 查看队尾 E getLast(); E peekLast();同样地:
- getXxx方法在队列为空时抛出异常
- peekXxx方法返回null
3. Deque的实现类比较
3.1 ArrayDeque实现
ArrayDeque是基于可变数组的实现,具有以下特点:
- 初始容量为16,自动扩容为原来的2倍
- 不允许插入null元素
- 非线程安全
- 作为栈使用时比Stack快,作为队列使用时比LinkedList快
Deque<Integer> deque = new ArrayDeque<>();3.2 LinkedList实现
LinkedList是基于双向链表的实现:
- 没有容量限制
- 允许插入null元素
- 同样非线程安全
- 实现了List、Deque等多个接口
Deque<String> deque = new LinkedList<>();3.3 实现类选择建议
| 选择依据 | ArrayDeque | LinkedList |
|---|---|---|
| 随机访问 | O(1) | O(n) |
| 插入/删除 | 通常更快 | 通常较慢 |
| 内存占用 | 更紧凑 | 每个元素需要额外节点对象 |
| null元素 | 不支持 | 支持 |
| 使用场景 | 高频操作、确定容量 | 需要List功能、不确定容量 |
4. Deque的典型应用场景
4.1 实现栈结构
虽然Java提供了Stack类,但官方文档推荐使用Deque作为栈的实现:
Deque<Integer> stack = new ArrayDeque<>(); // 入栈 stack.push(1); stack.push(2); // 出栈 int top = stack.pop();4.2 滑动窗口算法
在处理数组/链表的滑动窗口问题时,Deque是理想选择:
// 求滑动窗口最大值 public int[] maxSlidingWindow(int[] nums, int k) { Deque<Integer> deque = new LinkedList<>(); int[] result = new int[nums.length - k + 1]; for (int i = 0; i < nums.length; i++) { // 维护单调递减队列 while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) { deque.pollLast(); } deque.offerLast(i); // 移除超出窗口范围的索引 if (deque.peekFirst() <= i - k) { deque.pollFirst(); } // 记录当前窗口最大值 if (i >= k - 1) { result[i - k + 1] = nums[deque.peekFirst()]; } } return result; }4.3 工作窃取算法
在并发编程中,Deque可用于实现工作窃取模式:
// 生产者线程 Deque<Task> deque = new LinkedList<>(); deque.offerLast(new Task()); // 消费者线程(窃取其他队列的任务) if (!deque.isEmpty()) { Task task = deque.pollFirst(); // 处理任务 }5. 性能优化与注意事项
5.1 初始化容量优化
对于ArrayDeque,合理设置初始容量可以避免频繁扩容:
// 预估需要存储1000个元素 Deque<Item> deque = new ArrayDeque<>(1000);5.2 并发访问问题
标准Deque实现都不是线程安全的,多线程环境下需要额外同步:
Deque<Message> deque = new LinkedList<>(); // 使用Collections工具类包装 Deque<Message> safeDeque = Collections.synchronizedDeque(deque);或者使用并发包中的实现:
Deque<Job> deque = new ConcurrentLinkedDeque<>();5.3 与Stack类的对比
| 特性 | Deque作为栈 | Stack类 |
|---|---|---|
| 性能 | 更优 | 较差 |
| 设计 | 接口更现代 | 遗留类 |
| 同步 | 需要额外处理 | 方法级别同步 |
| 推荐度 | 官方推荐 | 不推荐使用 |
5.4 常见陷阱
- NPE问题:ArrayDeque不允许插入null,但LinkedList允许
- 容量限制:ArrayDeque有最大容量限制(Integer.MAX_VALUE - 8)
- 并发修改:迭代过程中修改队列会抛出ConcurrentModificationException
- 方法混淆:避免混用Queue和Deque的方法,保持风格一致
6. 源码分析与实现原理
6.1 ArrayDeque内部结构
ArrayDeque使用循环数组实现:
transient Object[] elements; // 存储元素的数组 transient int head; // 头部指针 transient int tail; // 尾部指针关键操作逻辑:
- 插入时计算
(tail + 1) & (elements.length - 1)确定新位置 - 扩容时调用
doubleCapacity()方法 - 通过位运算替代取模,提高效率
6.2 LinkedList节点结构
LinkedList使用标准的双向链表节点:
private static class Node<E> { E item; Node<E> next; Node<E> prev; // 构造方法... }所有操作都是通过调整节点引用来完成,没有容量限制。
7. 扩展应用与进阶技巧
7.1 实现阻塞双端队列
结合Lock和Condition可以实现阻塞版本的Deque:
public class BlockingDeque<E> { private final Deque<E> deque = new LinkedList<>(); private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); public void putFirst(E e) throws InterruptedException { lock.lock(); try { deque.addFirst(e); notEmpty.signal(); } finally { lock.unlock(); } } public E takeLast() throws InterruptedException { lock.lock(); try { while (deque.isEmpty()) { notEmpty.await(); } return deque.removeLast(); } finally { lock.unlock(); } } }7.2 实现优先级双端队列
通过组合PriorityQueue和Deque接口:
class PriorityDeque<E> extends AbstractQueue<E> implements Deque<E> { private final PriorityQueue<E> queue = new PriorityQueue<>(); // 实现所有Deque方法... }7.3 与Stream API结合
Deque可以方便地转换为Stream进行处理:
Deque<String> deque = new ArrayDeque<>(Arrays.asList("a", "b", "c")); List<String> filtered = deque.stream() .filter(s -> s.length() > 0) .collect(Collectors.toList());8. 最佳实践总结
- 接口引用原则:始终使用Deque接口声明变量,而不是具体实现类
- 方法明确性:优先使用xxxFirst/xxxLast方法,避免使用从Queue继承的模糊方法
- 实现选择:大多数情况下ArrayDeque是更好的选择,除非需要List功能
- 容量规划:对ArrayDeque预先设置合理的初始容量
- 线程安全:多线程环境下使用并发实现或手动同步
- 替代Stack:用Deque的push/pop方法实现栈功能
- 空值处理:注意不同实现类对null值的处理差异
- 性能监控:在频繁操作的场景下关注Deque的性能表现