优先队列在 Java 中是如何实现的?PriorityQueue 作为 Java 集合框架中的重要成员,其底层结构与普通队列完全不同。本文深入分析其核心机制。
1. PriorityQueue 基础概念
PriorityQueue 是一个基于优先级的队列,它会自动扩容直到达到 JVM 对数组大小的限制(理论上限是 Integer.MAX_VALUE - 8)。队列元素按照自然顺序或指定比较器排序,总是优先获取优先级最高的元素。

2. 底层数据结构:二叉堆
PriorityQueue 的核心是二叉堆,这是一种特殊的完全二叉树,在 Java 中通过数组实现。
二叉堆有两个关键特性:
- 结构性:除最后一层外都是完全填满的,最后一层从左到右填充
- 堆序性:每个节点都大于等于(大顶堆)或小于等于(小顶堆)其子节点
PriorityQueue 默认实现小顶堆,即根节点是最小元素。

3. 数组表示法
二叉堆虽然是树形结构,但 Java 中使用数组高效存储,通过索引计算父子关系:
- 对于索引 i 的节点:
- 父节点索引:(i-1) >>> 1 (无符号右移,等同于(i-1)/2 但性能更优)
- 左子节点索引:2*i+1
- 右子节点索引:2*i+2
java
public class PriorityQueue<E> extends AbstractQueue<E> {
// 存储元素的数组
transient Object[] queue;
// 队列中的元素数量
private int size = 0;
// 比较器
private final Comparator<? super E> comparator;
// 默认初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 构造函数...
}
注意,内部数组是Object[]
类型,而非泛型数组。这是 Java 集合类的常见实现模式,源于泛型引入前的历史原因。虽然 PriorityQueue 的公共方法确保类型安全,但内部实现中需要频繁进行类型转换(E)
,这是为了兼容性和性能考虑。
4. 核心操作实现
入队操作(offer/add)
当添加元素时,将其放在数组末尾,然后向上调整堆结构:
java
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
int i = size;
// 扩容检查
if (i >= queue.length)
grow(i + 1);
size = i + 1;
// 如果堆为空,直接插入第一个元素
if (i == 0)
queue[0] = e;
else
// 向上调整堆
siftUp(i, e);
return true;
}
// 向上调整堆的核心逻辑
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1; // 计算父节点位置,使用无符号右移而非除法提升性能
Object e = queue[parent];
if (key.compareTo((E) e) >= 0) // 如果当前元素大于等于父节点,停止调整
break;
queue[k] = e; // 父节点下移
k = parent;
}
queue[k] = key; // 放置元素到最终位置
}
当数组容量不足时,grow()
方法会触发扩容。它实现了一个智能增长策略:当原容量小于 64 时,新容量翻倍;否则增加 50%。这种策略平衡了内存利用率和重分配频率,避免了频繁的昂贵扩容操作,同时也不会过度浪费内存。
出队操作(poll)
移除堆顶元素(最小值),将最后一个元素放到堆顶,然后向下调整堆结构:
java
public E poll() {
if (size == 0)
return null;
int s = --size;
E result = (E) queue[0]; // 取出堆顶元素
E x = (E) queue[s]; // 最后一个元素
queue[s] = null; // 清空最后位置
if (s != 0)
siftDown(0, x); // 从顶部开始向下调整
return result;
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // 堆的一半位置(只需检查到非叶子节点)
while (k < half) { // 优化:索引>=half的节点都是叶子节点,无需下沉
int child = (k << 1) + 1; // 左子节点
Object c = queue[child];
int right = child + 1;
// 如果右子节点存在且小于左子节点,选择右子节点
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
// 如果当前元素小于等于最小的子节点,停止调整
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c; // 子节点上移
k = child;
}
queue[k] = key; // 放置元素到最终位置
}
5. 其他关键操作
批量构造与 heapify 过程
当从集合批量构造 PriorityQueue 时,并不是逐个调用 offer 方法(这将是 O(n log n)的复杂度),而是采用了一个更高效的 O(n)建堆算法:
java
private void heapify() {
// 从最后一个非叶子节点开始,自下而上进行调整
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
这个过程从倒数第一个非叶子节点开始,依次向上对每个节点执行 siftDown 操作。这种方法比逐个插入要高效得多,是一种巧妙的算法优化。

remove(Object o)方法
从队列中删除指定元素,这是一个 O(n)操作:
java
public boolean remove(Object o) {
int i = indexOf(o); // O(n)线性查找元素位置
if (i == -1)
return false;
else {
removeAt(i); // 从指定位置移除元素
return true;
}
}
private E removeAt(int i) {
// 最后一个元素作为"洞"
int s = --size;
if (s == i) // 如果移除的是最后一个元素
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved); // 尝试下沉
if (queue[i] == moved) // 如果位置没变(说明不需要下沉)
siftUp(i, moved); // 则尝试上浮
return moved;
}
return null;
}
这里有一个微妙但重要的逻辑:if (queue[i] == moved) siftUp(i, moved);
。为什么在siftDown
之后还需要siftUp
?这是因为siftDown
操作会将合适的子节点上移来填充位置i
处的空洞。但如果替换元素moved
没有子节点,或者比它的所有子节点都小,那么siftDown
不会执行任何操作,此时queue[i]
仍然等于moved
。
在这种情况下,可能moved
元素比它的新父节点还小(特别是在删除了一个较大的中间元素后),因此需要通过siftUp
向上调整来恢复堆的性质。这个双向调整机制保证了在任何情况下移除元素后堆的正确性。
迭代器行为
PriorityQueue 的迭代器不保证按优先级顺序遍历元素,它只是按照底层数组的顺序返回元素:
java
public Iterator<E> iterator() {
return new Itr();
}
private final class Itr implements Iterator<E> {
// 简单遍历数组,不保证按优先级顺序
// 如需按优先级处理所有元素,应使用poll()方法
}
6. 实际应用案例
案例一:任务调度系统
假设我们需要实现一个基于优先级的任务调度系统:
java
import java.util.PriorityQueue;
import java.util.Comparator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TaskScheduler {
private static final Logger logger = LoggerFactory.getLogger(TaskScheduler.class);
private PriorityQueue<Task> taskQueue;
public TaskScheduler() {
// 创建优先队列,使用Comparator.comparingInt避免整数溢出风险
taskQueue = new PriorityQueue<>(Comparator.comparingInt(Task::getPriority));
}
public void addTask(Task task) {
taskQueue.offer(task);
logger.info("任务已添加: {}, 优先级: {}", task.getName(), task.getPriority());
}
public Task getNextTask() {
return taskQueue.poll();
}
public static void main(String[] args) {
try {
TaskScheduler scheduler = new TaskScheduler();
scheduler.addTask(new Task("数据备份", 3));
scheduler.addTask(new Task("系统更新", 1));
scheduler.addTask(new Task("日志清理", 2));
// 按优先级获取任务
Task task;
while ((task = scheduler.getNextTask()) != null) {
logger.info("执行任务: {}, 优先级: {}", task.getName(), task.getPriority());
}
// 模拟异常情况,演示异常日志处理
scheduler.addTask(null); // 会抛出NPE
} catch (Exception e) {
// 正确记录异常,包含完整堆栈信息,这对排查生产问题至关重要
logger.error("调度器执行过程中发生意外错误", e);
}
}
static class Task {
private String name;
private int priority; // 数字越小优先级越高
public Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
public String getName() {
return name;
}
public int getPriority() {
return priority;
}
}
}
注意,我们使用Comparator.comparingInt
作为比较器,这是 Java 8 引入的函数式接口,它实现了策略模式(Strategy Pattern)。这种设计将比较逻辑("策略")与数据结构解耦,允许灵活配置排序行为(小顶堆、大顶堆、自定义对象排序)而无需修改 PriorityQueue 的核心实现。
案例二:Dijkstra 最短路径算法
PriorityQueue 在图算法中的应用,将算法复杂度从 O(V²)优化到 O(E log V):
java
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DijkstraAlgorithm {
private static final Logger logger = LoggerFactory.getLogger(DijkstraAlgorithm.class);
public static void findShortestPaths(int[][] graph, int start) {
int n = graph.length;
int[] distance = new int[n];
boolean[] visited = new boolean[n];
Arrays.fill(distance, Integer.MAX_VALUE);
distance[start] = 0;
// 使用PriorityQueue优化选择最小距离节点的过程
PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparingInt(node -> node.distance));
pq.offer(new Node(start, 0));
while (!pq.isEmpty()) {
Node current = pq.poll();
int u = current.vertex;
// 如果已访问过,跳过
if (visited[u]) continue;
visited[u] = true;
// 更新相邻节点距离
for (int v = 0; v < n; v++) {
if (graph[u][v] > 0 && !visited[v]) {
int newDist = distance[u] + graph[u][v];
if (newDist < distance[v]) {
distance[v] = newDist;
pq.offer(new Node(v, newDist));
}
}
}
}
// 输出结果
for (int i = 0; i < n; i++) {
logger.info("从节点 {} 到节点 {} 的最短距离是: {}", start, i, distance[i]);
}
}
static class Node {
int vertex;
int distance;
Node(int vertex, int distance) {
this.vertex = vertex;
this.distance = distance;
}
}
public static void main(String[] args) {
try {
int[][] graph = {
{0, 4, 0, 0, 0, 0, 0, 8, 0},
{4, 0, 8, 0, 0, 0, 0, 11, 0},
{0, 8, 0, 7, 0, 4, 0, 0, 2},
{0, 0, 7, 0, 9, 14, 0, 0, 0},
{0, 0, 0, 9, 0, 10, 0, 0, 0},
{0, 0, 4, 14, 10, 0, 2, 0, 0},
{0, 0, 0, 0, 0, 2, 0, 1, 6},
{8, 11, 0, 0, 0, 0, 1, 0, 7},
{0, 0, 2, 0, 0, 0, 6, 7, 0}
};
findShortestPaths(graph, 0);
} catch (Exception e) {
logger.error("执行Dijkstra算法时发生错误", e);
}
}
}
7. 大顶堆与小顶堆
PriorityQueue 默认实现小顶堆,但通过自定义比较器可以轻松实现大顶堆:
java
// 方法一:使用Collections.reverseOrder()
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// 方法二:使用Lambda比较器(安全写法,避免整数溢出)
PriorityQueue<Integer> maxHeap2 = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
// 方法三:使用Comparator工厂方法
PriorityQueue<Integer> maxHeap3 = new PriorityQueue<>(Comparator.reverseOrder());
8. 常见陷阱与最佳实践
-
迭代器顺序陷阱:PriorityQueue 的迭代器不保证按优先级顺序遍历元素。如需按优先级处理,应循环调用 poll()方法。
java// 错误:这不会按优先级顺序处理元素 for (Task task : taskQueue) { processTask(task); } // 正确:按优先级顺序处理所有元素 Task task; while ((task = taskQueue.poll()) != null) { processTask(task); }
-
元素修改陷阱:修改已在队列中对象的排序关键属性,会破坏堆结构。
java// 错误:直接修改队列中任务的优先级 Task task = findTaskInQueue(); task.setPriority(newPriority); // 堆结构已被破坏! // 正确:移除后修改再重新入队 Task task = findTaskInQueue(); taskQueue.remove(task); // 先移除 task.setPriority(newPriority); // 修改 taskQueue.offer(task); // 重新入队
-
线程安全性:PriorityQueue 不是线程安全的。多线程环境下应使用 PriorityBlockingQueue。
java// 多线程环境中使用 import java.util.concurrent.PriorityBlockingQueue; PriorityBlockingQueue<Task> safeTaskQueue = new PriorityBlockingQueue<>(11, Comparator.comparingInt(Task::getPriority));
-
比较器陷阱:避免使用减法实现比较器,防止整数溢出。
java// 危险:可能导致整数溢出 new PriorityQueue<>((a, b) -> a.getValue() - b.getValue()); // 安全:使用Integer.compare或Comparator工厂方法 new PriorityQueue<>(Comparator.comparingInt(Task::getValue));
9. 性能分析
PriorityQueue 的主要操作性能表现:

10. 总结
特性 | 说明 |
---|---|
底层结构 | 二叉堆(通过数组实现的完全二叉树) |
默认排序 | 小顶堆(元素自然顺序) |
排序定制 | 可通过自定义 Comparator 实现大顶堆或其他排序 |
元素要求 | 不允许 null,元素必须可比较或提供比较器 |
线程安全 | 否(多线程环境应使用 PriorityBlockingQueue) |
构造时间复杂度 | 批量构造 O(n),逐个添加 O(n log n) |
插入时间复杂度 | O(log n) |
删除时间复杂度 | O(log n) |
查找时间复杂度 | O(n) |
迭代器顺序 | 不保证按优先级顺序 |
主要应用 | 任务调度、图算法、事件处理、优先级处理 |
通过对 PriorityQueue 底层结构与源码的深入理解,我们可以更高效地利用它解决各种问题,同时避开常见陷阱。
究其本质,PriorityQueue 体现了经典的工程权衡:它牺牲了 O(1)的入队/出队操作和 O(n)的查找,以换取对优先级最高元素的高效 O(log n)访问,使其成为算法和系统中不可或缺的工具,尤其是在"下一步"由重要性而非到达时间决定的场景中。