一次把栈、队列、双端队列、优先队列、链表讲清楚 ------ 从"能不能用 Deque 全包"到"实际该怎么选"。
一、起因:Deque 是不是万能容器?
问题 :Java 里双端队列 Deque 的功能是不是完全囊括了普通栈和队列?实际开发能不能全用 Deque?
结论:
- 功能上 :
Deque确实是栈和队列的超集,理论上可以只用它。 - 实践上 :不建议无脑全用,要按语义选接口。
- 例外 :优先队列、阻塞队列、线程安全队列等,
Deque替代不了。
一句话:Deque 是"能",但不是"该"。工具选最贴合语义的那个。
二、ArrayDeque 常用方法
ArrayDeque 是 Deque 接口最常用的实现,底层是可扩容循环数组 ,非线程安全,作为栈/队列时性能优于 Stack 和 LinkedList。
两端命名
addFirst / offerFirst addLast / offerLast
↓ ↓
[ head ] ← ... 循环数组 ... → [ tail ]
↑ ↑
removeFirst / pollFirst removeLast / pollLast
peekFirst peekLast
方法分组
| 功能 | 头部 | 尾部 |
|---|---|---|
| 插入 | addFirst / offerFirst |
addLast / offerLast |
| 删除 | removeFirst / pollFirst |
removeLast / pollLast |
| 查看 | getFirst / peekFirst |
getLast / peekLast |
抛异常 vs 返回特殊值(口诀)
add/remove/get→ 失败抛异常offer/poll/peek→ 失败返回特殊值 (false/null)
快捷方法
| 角色 | 方法 |
|---|---|
| 栈 | push / pop / peek |
| 队列 | offer / poll / peek |
注意事项
- 不能存
null,会抛NullPointerException。 - 非线程安全 ,多线程用
ConcurrentLinkedDeque。 - 随机访问 O(n),没有下标 O(1) 访问。
- 作为栈/队列,比
Stack(同步开销)和LinkedList(缓存不友好)都快。
三、为什么有两种声明写法?------ 面向接口编程
java
Deque<Integer> stack = new ArrayDeque<>(); // ✅ 推荐
ArrayDeque<Integer> stack = new ArrayDeque<>(); // 也能跑,但少见
- 左边 = 引用类型(编译期):决定能调用哪些方法。
- 右边 = 对象类型(运行期):决定实际行为。
编译看左边,运行看右边。
为什么推荐"接口在左,实现在右"
- 解耦:换实现只改右边一处。
- 缩小暴露面:只暴露必要方法,减少误用。
- 表达意图:一眼看出变量扮演什么角色。
- 便于测试/依赖倒置:方法参数用接口,可传任意实现。
什么时候用具体类声明
- 需要该类接口之外的特有方法 (如
ArrayList.ensureCapacity)。 - 局部变量、生命周期极短、不涉及替换。
- 对
ArrayDeque来说几乎总是用Deque声明。
四、优先队列 PriorityQueue
声明
java
Queue<Integer> pq = new PriorityQueue<>(); // ✅ 推荐
不能 写成 Deque<Integer> pq = ...,因为 PriorityQueue 只实现 Queue,没实现 Deque。
为什么 Deque 替代不了它
| 容器 | 出队规则 |
|---|---|
| 普通队列 | 先进先出(FIFO) |
| 双端队列 | 两端进出,按位置 |
| 优先队列 | 按优先级,与入队顺序无关 |
Deque 的 offerFirst/pollLast 对优先队列毫无意义------元素位置由优先级决定。
常用方法
| 方法 | 说明 |
|---|---|
offer / add |
入队 |
poll / remove |
出队(返回优先级最高元素) |
peek / element |
查看队首 |
关键 :poll/peek 拿到的是优先级最高 的元素,不是最早入队的;不允许 null;非线程安全。
默认小顶堆
java
Queue<Integer> pq = new PriorityQueue<>(); // 默认小顶堆
自定义优先级:
java
Queue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a); // 大顶堆
Queue<String> byLen = new PriorityQueue<>(Comparator.comparingInt(String::length));
典型场景
- 第 K 大 / 第 K 小、Top K 高频
- Dijkstra 最短路
- 合并 K 个有序链表
- 数据流中位数(对顶堆)
五、LinkedList 详解
继承体系
java
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable
同时实现 List 和 Deque,是"多面手":
java
List<Integer> list = new LinkedList<>(); // 当 List
Deque<Integer> deque = new LinkedList<>(); // 当栈/队列
Queue<Integer> queue = new LinkedList<>(); // 当队列
底层结构
双向链表,每个节点存 prev、item、next,额外维护 first / last 引用。没有下标数组。
性能特征
| 操作 | LinkedList | ArrayList | ArrayDeque |
|---|---|---|---|
| 尾部插入 | O(1) | 均摊 O(1) | 均摊 O(1) |
| 头部插入/删除 | O(1) | O(n) | O(1) |
按下标 get(i) |
O(n) | O(1) | O(n) |
| 内存开销 | 大 | 小 | 小 |
| 缓存友好性 | 差 | 好 | 好 |
LinkedList vs ArrayDeque
| 维度 | LinkedList | ArrayDeque |
|---|---|---|
| 底层 | 双向链表 | 循环数组 |
| 栈/队列性能 | 较慢 | 更快 |
| 内存 | 大 | 小 |
存 null |
允许 | 不允许 |
| 实现 List | ✅ | ❌ |
结论 :当栈/队列优先 ArrayDeque;需存 null 才用 LinkedList。
常见误区
- ❌ 插入删除总是 O(1):
add(index,e)仍要 O(n) 找位置。 - ❌ LinkedList 比 ArrayList 快:大多数场景 ArrayList 更快(缓存友好)。
- ❌ 当栈/队列用 LinkedList 没问题:能跑,但
ArrayDeque更快。 - ❌
for (int i=0;i<n;i++) list.get(i):O(n²) 陷阱,用 for-each。
六、三种容器声明对照表
| 容器 | 推荐声明 | 实现 | 声明为 Deque? |
|---|---|---|---|
| 栈 | Deque<T> |
new ArrayDeque<>() |
✅ 可以 |
| 队列 | Queue<T> |
new ArrayDeque<>() |
✅ 可以 |
| 优先队列 | Queue<T> |
new PriorityQueue<>() |
❌ 不行 |
| 列表 | List<T> |
new ArrayList<>() |
--- |
规律:
- 能用哪个接口声明,取决于右边实现类实现了哪些接口。
- 左边接口决定"能调什么",右边实现决定"实际怎么做"。
七、最终选型口诀
| 需求 | 选择 |
|---|---|
| 列表 | ArrayList |
| 栈 / 队列 / 双端队列 | ArrayDeque |
| 优先级 | PriorityQueue |
需要存 null / 频繁头插 |
LinkedList |
| 线程安全 | ConcurrentLinkedDeque、PriorityBlockingQueue 等 |
核心原则:
- 接口在左,实现在右(面向接口编程)。
- 按语义选容器,而不是"哪个万能用哪个"。
- Deque 是栈和队列的超集,但不是优先队列的替代品。
- 默认选 ArrayDeque / ArrayList ,
LinkedList只在有特殊需求时用。
八、一句话收尾
Deque一个接口,栈、队列、双端队列三种角色全包;PriorityQueue管优先级;LinkedList是多面手但常被误用。能用一个容器做多件事,不代表应该------代码是给人看的,语义清晰比"省一个类"重要得多。
本文整理自一次关于 Java 容器选型的讨论,涵盖 Deque、ArrayDeque、PriorityQueue、LinkedList 的对比与实战建议。