JDK 8 中的 PriorityQueue
是基于二叉堆(最小堆,默认)实现的优先队列,它支持高效的按优先级插入和删除元素操作(时间复杂度为 O(log n)
平均时间)。与普通队列(FIFO)不同,PriorityQueue
的出队顺序由元素的优先级决定(自然顺序或自定义比较器),适用于需要“按优先级处理”的场景(如任务调度、事件优先级队列)。
一、PriorityQueue 的核心特性
特性 | 说明 |
---|---|
优先队列 | 元素按自然顺序(若实现 Comparable )或自定义 Comparator 排序,默认最小堆。 |
无界队列 | 底层数组动态扩容,理论上容量无上限(受内存限制)。 |
不保证迭代顺序 | 迭代器遍历顺序与堆结构无关(仅保证出队顺序按优先级)。 |
非线程安全 | 多线程环境下需手动同步(如 Collections.synchronizedSet )。 |
允许 null 元素 | 若使用自然排序(无 Comparator ),插入 null 会抛出 NullPointerException ;若使用自定义 Comparator ,需处理 null 。 |
二、PriorityQueue 的核心结构
1. 继承与接口
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable
AbstractQueue
:提供Queue
接口的骨架实现(如add
、remove
等方法的默认逻辑)。Serializable
:支持序列化。
2. 核心属性
PriorityQueue
的底层通过动态数组存储堆结构,核心属性如下:
// 存储堆元素的数组(初始容量为 11,或通过构造方法指定)
transient Object[] queue;
// 元素数量
private int size = 0;
// 比较器(若为 null,使用元素的自然排序)
private final Comparator<? super E> comparator;
// 序列化版本号
private static final long serialVersionUID = -7720805057307804929L;
3. 堆的性质
PriorityQueue
使用二叉堆(完全二叉树)存储元素,通过数组实现:
- 父节点与子节点索引关系:
- 父节点索引:
(i - 1) >>> 1
(等价于(i - 1) / 2
,无符号右移避免负数)。 - 左子节点索引:
(i << 1) + 1
(等价于2 * i + 1
)。 - 右子节点索引:
(i << 1) + 2
(等价于2 * i + 2
)。
- 父节点索引:
- 堆特性:父节点的优先级总是高于(或等于)子节点(最小堆);若使用自定义
Comparator
,则按比较器规则调整。
三、构造方法
PriorityQueue
提供了多种构造方法,核心是初始化堆数组和比较器:
1. 默认构造方法(自然排序,初始容量 11)
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
if (initialCapacity < 1) {
throw new IllegalArgumentException();
}
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
2. 指定初始容量的构造方法
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
3. 从集合初始化(按自然排序或比较器)
public PriorityQueue(Collection<? extends E> c) {
this(comparator = null); // 默认自然排序
addAll(c); // 添加所有元素并调整堆
}
public PriorityQueue(SortedSet<? extends E> s) {
this(s.comparator()); // 继承 SortedSet 的比较器
addAll(s);
}
4. 从迭代器初始化(按自然排序)
public PriorityQueue(Iterator<? extends E> c) {
this(); // 默认自然排序
while (c.hasNext()) {
add(c.next());
}
}
四、核心方法解析
PriorityQueue
的核心操作围绕堆的插入、删除和调整展开,以下是最常用方法的源码解析:
1. add(E e) / offer(E e):添加元素
add
和 offer
功能相同(PriorityQueue
是无界队列,offer
始终返回 true
),核心逻辑是将元素插入堆末尾,然后通过**上浮(siftUp)**调整堆结构。
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null) {
throw new NullPointerException(); // 不允许 null 元素(自然排序时)
}
modCount++;
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;
}
// 上浮调整(从索引 i 开始,与父节点比较)
private void siftUp(int k, E x) {
if (comparator != null) {
// 使用自定义比较器
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0) { // x 优先级不高于父节点,停止调整
break;
}
queue[k] = e; // 父节点下移
k = parent;
}
} else {
// 使用自然排序(元素必须实现 Comparable)
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) { // x 优先级不高于父节点,停止调整
break;
}
queue[k] = e; // 父节点下移
k = parent;
}
}
queue[k] = x; // 插入最终位置
}
2. poll():移除并返回队首元素(最小/最大)
poll
是核心方法,用于取出堆顶元素(优先级最高的元素)。若堆为空,返回 null
。核心逻辑是:
- 取出堆顶元素(
queue[0]
)。 - 将堆末尾元素移到堆顶(
queue[0] = queue[size-1]
)。 - 通过**下沉(siftDown)**调整堆结构。
public E poll() {
if (size == 0) {
return null;
}
int s = --size;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null; // 清除末尾元素引用(GC 友好)
if (s != 0) { // 堆非空,下沉调整
siftDown(0, x);
}
return result;
}
// 下沉调整(从索引 i 开始,与子节点比较)
private void siftDown(int k, E x) {
int half = size >>> 1; // 堆的最后一个非叶子节点索引(half = size/2 - 1)
while (k < half) {
int left = (k << 1) + 1; // 左子节点索引
int right = left + 1; // 右子节点索引
int smallest = left; // 假设左子节点最小
// 比较左、右子节点,找到最小的
if (right < size && comparatorCompare(queue[right], queue[left]) < 0) {
smallest = right;
}
// 比较当前节点与最小子节点
if (comparatorCompare(x, queue[smallest]) <= 0) { // x 优先级不高于最小子节点,停止调整
break;
}
queue[k] = queue[smallest]; // 最小子节点上移
k = smallest;
}
queue[k] = x; // 插入最终位置
}
// 辅助方法:使用比较器比较两个元素
private int comparatorCompare(Object a, Object b) {
return comparator != null ? comparator.compare((E) a, (E) b) : ((Comparable<? super E>) a).compareTo((E) b);
}
3. peek():查看队首元素(不删除)
peek
方法返回堆顶元素(优先级最高的元素),若堆为空则返回 null
:
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
4. remove(Object o):删除指定元素
remove
方法删除堆中指定的元素(若存在),返回 true
;否则返回 false
。核心逻辑是:
- 找到元素的索引(通过遍历或辅助数据结构,但
PriorityQueue
未优化此操作,时间复杂度为O(n)
)。 - 将该元素与堆末尾元素交换,然后下沉调整堆。
public boolean remove(Object o) {
int i;
if ((i = indexOf(o)) != -1) {
E x = (E) queue[i];
int s = --size;
if (s == i) { // 元素在堆顶,直接下沉
queue[i] = null;
if (s > 0) {
siftDown(0, x);
}
} else { // 元素在中间或末尾,交换到末尾后下沉
E moved = (E) queue[s];
queue[s] = null;
if (comparator == null) {
siftDownWithComparator(i, moved, queue, size); // 使用比较器下沉
} else {
siftDown(i, moved); // 使用自然排序下沉(可能有问题,需看具体实现)
}
}
return true;
}
return false;
}
// 辅助方法:查找元素的索引(线性遍历,O(n) 时间)
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++) {
if (o.equals(queue[i])) {
return i;
}
}
}
return -1;
}
5. iterator():返回迭代器
PriorityQueue
的迭代器按堆的底层数组顺序遍历元素(非优先级顺序),迭代过程中若队列被修改(非通过迭代器的 remove
),会抛出 ConcurrentModificationException
(快速失败机制)。
public Iterator<E> iterator() {
return new Itr();
}
private final class Itr implements Iterator<E> {
private int cursor = 0; // 下一个要访问的索引
public boolean hasNext() {
return cursor < size;
}
public E next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return (E) queue[cursor++];
}
}
五、堆的扩容机制(grow)
当堆的容量不足时(size >= queue.length
),触发扩容:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 新容量为原容量的 2 倍(若原容量 < 64)或原容量 + 原容量/4(若原容量 >= 64)
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
// 限制最大容量(避免内存溢出)
if (newCapacity > MAX_ARRAY_SIZE) {
newCapacity = hugeCapacity(minCapacity);
}
queue = Arrays.copyOf(queue, newCapacity);
}
private int hugeCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
}
return (MAX_ARRAY_SIZE >= minCapacity) ? MAX_ARRAY_SIZE : Integer.MAX_VALUE;
}
六、关键源码细节
1. 序列化与反序列化
PriorityQueue
实现了 Serializable
接口,序列化时保存 queue
数组和 comparator
;反序列化时重建堆结构:
// 序列化时写入 queue 和 comparator
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);
for (int i = 0; i < size; i++) {
s.writeObject(queue[i]);
}
}
// 反序列化时读取并重建堆
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
int size = s.readInt();
queue = new Object[Math.max(size, 11)]; // 至少初始容量 11
for (int i = 0; i < size; i++) {
queue[i] = s.readObject();
}
this.size = size;
heapify(); // 重建堆结构
}
// 从数组重建堆(下沉所有非叶子节点)
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--) {
siftDown(i, (E) queue[i]);
}
}
七、性能与注意事项
1. 性能特点
- 插入(add/offer):平均时间复杂度
O(log n)
(堆调整的上浮操作)。 - 删除(poll/remove):平均时间复杂度
O(log n)
(堆调整的下沉操作)。 - 查找队首(peek):时间复杂度
O(1)
(直接访问堆顶)。 - 迭代(iterator):时间复杂度
O(n)
(遍历数组),但顺序不保证。
2. 注意事项
- 非线程安全:多线程环境下同时修改
PriorityQueue
可能导致数据不一致(如ConcurrentModificationException
)。解决方案:Queue<E> synchronizedQueue = Collections.synchronizedQueue(new PriorityQueue<>());
- 迭代顺序不确定:迭代器遍历的是堆的底层数组,与优先级顺序无关。若需按优先级遍历,需通过
poll()
逐个取出元素。 null
元素限制:若使用自然排序(无Comparator
),插入null
会抛出NullPointerException
;若使用自定义Comparator
,需确保比较器能处理null
。- 扩容开销:扩容时需要复制数组,频繁插入可能导致性能下降(可通过预分配足够容量优化)。
八、总结
PriorityQueue
是基于二叉堆的高效优先队列,核心优势是按优先级插入和删除元素(O(log n)
时间复杂度)。其内部通过动态数组存储堆结构,通过上浮和下沉操作维护堆的性质。实际开发中,若需要按优先级处理任务(如事件调度、资源分配),PriorityQueue
是首选;若需频繁迭代或保证迭代顺序,需谨慎使用(或改用其他数据结构)。