PriorityQueue 源码深度分析(Java 8)
PriorityQueue 是 Java 集合框架中基于二叉优先级堆实现的无界队列,核心特性是元素按优先级排序(非 FIFO),每次出队(poll())返回优先级最高(默认最小)的元素。本文从底层结构、核心属性、关键方法、堆维护逻辑等角度,结合源码拆解其实现原理。
一、核心设计理念与底层结构
1. 数据结构基础:二叉小顶堆(默认)
PriorityQueue 的底层是完全二叉树(所有层除最后一层外均满,最后一层左对齐),并用数组存储(高效索引父 / 子节点)。核心索引关系(基于 0 开始的数组):
- 父节点索引
i→ 左子节点2i + 1、右子节点2i + 2 - 子节点索引
i→ 父节点(i - 1) >>> 1(无符号右移,等价于整数除法(i-1)/2)
默认是小顶堆(父节点 < 左右子节点),可通过传入 Comparator 改为大顶堆。
2. 核心属性(源码字段)
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
// 默认初始容量(无参构造器使用)
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 存储堆元素的数组(动态扩容)
transient Object[] elementData;
// 当前队列元素个数
private int size = 0;
// 比较器:null 表示使用元素自然排序(需实现 Comparable)
private final Comparator<? super E> comparator;
// 最大数组容量(避免 OOM,Integer.MAX_VALUE - 8)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
二、构造方法:初始化与堆化
PriorityQueue 提供 5 个核心构造器,核心逻辑是初始化数组容量和构建堆结构(heapify 操作)。
1. 关键构造器源码解析
(1)无参构造器(默认容量 11,自然排序)
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
(2)指定初始容量 + 比较器
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.elementData = new Object[initialCapacity];
this.comparator = comparator;
}
(3)传入集合(核心:堆化 heapify)
如果传入的集合是 SortedSet 或 PriorityQueue,直接复用其比较器;否则调用 heapify 将普通集合转为堆:
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
} else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<?>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
} else {
this.comparator = null;
// 先复制元素到数组,再堆化
initElementsFromCollection(c);
heapify();
}
}
2. 堆化(heapify()):普通数组 → 堆
堆化的核心是从最后一个非叶子节点开始,依次向前执行下沉(siftDown)操作。原因:叶子节点本身是合法堆,非叶子节点的子树需调整为堆。
private void heapify() {
// 最后一个非叶子节点索引:(size/2) - 1
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) elementData[i]);
}
例:数组 [5,3,1,2,4] 堆化过程:
- 最后一个非叶子节点索引
(5/2)-1 = 1(元素 3),执行siftDown; - 再处理索引 0(元素 5),执行
siftDown; - 最终得到小顶堆
[1,2,5,3,4]。
三、核心操作:入队、出队与堆维护
PriorityQueue 的核心是入队(offer)时上浮(siftUp)、出队(poll)时下沉(siftDown),通过这两个操作保证堆的性质。
1. 入队(offer(E e)):添加元素并上浮
PriorityQueue 是无界队列,offer 与 add 等价(均不抛容量不足异常),核心步骤:
- 检查元素非 null(不允许 null);
- 扩容(若
size >= 数组容量); - 元素添加到数组末尾(
size位置); - 上浮(
siftUp)调整堆。
源码实现
public boolean offer(E e) {
if (e == null)
throw new NullPointerException(); // 禁止 null 元素
modCount++;
int i = size;
if (i >= elementData.length)
grow(i + 1); // 扩容(需容纳 i+1 个元素)
size = i + 1;
if (i == 0)
elementData[0] = e; // 空队列直接放堆顶
else
siftUp(i, e); // 上浮调整
return true;
}
上浮(siftUp):从末尾向上调整
目的:让新元素(插入在数组末尾)找到合适位置,保证父节点优先级高于子节点。
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x); // 自定义比较器
else
siftUpComparable(k, x); // 自然排序(元素需实现 Comparable)
}
// 自然排序版上浮
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 = elementData[parent];
// 新元素 >= 父节点,满足堆性质,退出
if (key.compareTo((E) e) >= 0)
break;
// 新元素 < 父节点,交换位置
elementData[k] = e;
k = parent; // 继续向上比较父节点的父节点
}
elementData[k] = key;
}
2. 出队(poll()):移除堆顶并下沉
核心步骤:
- 空队列返回 null;
- 取堆顶元素(
elementData[0],优先级最高); - 将数组末尾元素移到堆顶(
elementData[0] = elementData[--size]); - 下沉(
siftDown)调整堆; - 返回原堆顶元素。
源码实现
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) elementData[0]; // 堆顶元素(结果)
E x = (E) elementData[s]; // 数组末尾元素
elementData[s] = null; // 释放引用(避免内存泄漏)
if (s != 0)
siftDown(0, x); // 下沉调整
return result;
}
下沉(siftDown):从堆顶向下调整
目的:让堆顶元素(替换后的末尾元素)找到合适位置,保证父节点优先级高于子节点。
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) {
// 1. 找到左右子节点中较小的那个(小顶堆)
int left = (k << 1) + 1; // 左子节点索引
int right = left + 1; // 右子节点索引
Object c = elementData[left];
if (right < size && ((Comparable<? super E>) c).compareTo((E) elementData[right]) > 0)
c = elementData[right]; // 右子节点更小,取右子节点
// 2. 父节点 <= 子节点,满足堆性质,退出
if (key.compareTo((E) c) <= 0)
break;
// 3. 父节点 > 子节点,交换位置
elementData[k] = c;
k = left; // 继续向下比较子节点的子节点
}
elementData[k] = key;
}
3. 扩容(grow(int minCapacity)):动态调整数组大小
当入队时 size >= 数组容量,触发扩容,扩容规则:
- 若当前容量 < 64:扩容为
2 * 容量 + 2(快速扩容); - 若当前容量 >= 64:扩容为
1.5 * 容量(平缓扩容); - 最大容量不超过
MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8),超过则用Integer.MAX_VALUE。
源码实现
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 扩容规则:小容量加倍+2,大容量1.5倍
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
// 检查是否超过最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 数组拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 溢出
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
四、其他重要方法
1. peek():查看堆顶(不移除)
直接返回数组第一个元素,时间复杂度 O (1):
public E peek() {
return (size == 0) ? null : (E) elementData[0];
}
2. remove(Object o):移除指定元素
步骤:
- 遍历数组找到元素索引
i; - 用末尾元素替换索引
i的元素; - 根据元素位置判断是上浮(
siftUp)还是下沉(siftDown); - 释放末尾元素引用。
源码核心逻辑
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
int s = --size;
if (s == i) // 移除的是末尾元素,直接置空
elementData[i] = null;
else {
E moved = (E) elementData[s];
elementData[s] = null;
siftDown(i, moved); // 先尝试下沉
// 若下沉后元素未变,说明需要上浮
if (elementData[i] == moved)
siftUp(i, moved);
}
return true;
}
3. contains(Object o):判断是否包含元素
遍历数组查找,时间复杂度 O (n):
public boolean contains(Object o) {
return indexOf(o) != -1;
}
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
五、关键特性与注意事项
1. 排序规则
- 自然排序:元素必须实现
Comparable接口,否则抛ClassCastException; - 自定义排序:通过构造器传入
Comparator(例:new PriorityQueue<>(Comparator.reverseOrder())实现大顶堆)。
2. 线程安全性
-
PriorityQueue 线程不安全,多线程并发修改(入队 / 出队)需外部同步:
// 线程安全包装 Queue<E> safePq = Collections.synchronizedQueue(new PriorityQueue<>());线程安全场景推荐使用
java.util.concurrent.PriorityBlockingQueue(基于 PriorityQueue 实现,支持阻塞)。
3. 迭代器无序性
iterator() 返回的迭代器不保证按优先级顺序遍历(仅遍历数组原始顺序)。若需有序遍历,需多次调用 poll()(会修改队列),或复制到数组后排序:
PriorityQueue<Integer> pq = new PriorityQueue<>(Arrays.asList(3,1,2));
// 有序遍历(破坏队列)
while (!pq.isEmpty()) {
System.out.println(pq.poll()); // 1,2,3
}
// 有序遍历(不破坏队列)
Integer[] arr = pq.toArray(new Integer[0]);
Arrays.sort(arr);
4. 时间复杂度
| 方法 | 时间复杂度 | 说明 |
|---|---|---|
offer()/add() | O(log n) | 上浮调整堆 |
poll() | O(log n) | 下沉调整堆 |
peek() | O(1) | 直接访问堆顶 |
contains() | O(n) | 遍历数组查找 |
remove(Object) | O(n) | 遍历查找 + O (log n) 堆调整 |
六、典型应用场景
- 任务调度:按任务优先级执行(例:线程池的延迟队列
DelayQueue基于 PriorityQueue 实现); - TopK 问题:找最大 / 最小的 k 个元素(例:用小顶堆找最大 k 个元素,时间复杂度 O (n log k));
- 最短路径算法:Dijkstra 算法中,用优先级队列选择当前最短路径节点。
总结
PriorityQueue 的核心是基于数组的二叉小顶堆,通过 siftUp(入队)和 siftDown(出队)维护堆的性质,保证优先级最高的元素始终在堆顶。其设计权衡了入队 / 出队的高效性(O (log n)),但牺牲了查找和遍历的效率(O (n)),适用于需要按优先级处理元素的场景。
关键记住:数组存储堆 + 上浮下沉调堆 + 动态扩容 是 PriorityQueue 的核心实现逻辑。

&spm=1001.2101.3001.5002&articleId=155382092&d=1&t=3&u=c729fbc4c7514ec0bf887a365dd6dba0)
1404

被折叠的 条评论
为什么被折叠?



