深度剖析:Java PriorityQueue 使用原理大揭秘
一、引言
在 Java 编程的世界里,数据结构犹如构建程序大厦的基石,为我们提供了高效存储和操作数据的方式。其中,PriorityQueue
(优先队列)是一个非常实用且强大的数据结构,它能够根据元素的优先级对元素进行排序,使得每次取出的元素都是优先级最高(或最低)的元素。
PriorityQueue
在很多场景下都有着广泛的应用,比如任务调度系统中,根据任务的优先级安排任务的执行顺序;在图算法中,用于实现 Dijkstra 算法来寻找最短路径等。了解 PriorityQueue
的使用原理,不仅能让我们在实际开发中更加得心应手地运用它,还能帮助我们深入理解 Java 集合框架的设计思想。
本文将深入到 PriorityQueue
的源码层面,全方位剖析其使用原理。我们将从基本概念入手,详细探讨其继承关系、核心属性和构造函数,接着深入分析各种操作方法的实现细节,包括元素的插入、删除、查看等。同时,我们还会关注 PriorityQueue
的性能表现以及线程安全特性。通过本文的学习,你将对 Java PriorityQueue
有一个全面而深入的理解,能够在实际开发中更加灵活地运用它。
二、PriorityQueue 概述
2.1 基本概念
PriorityQueue
是 Java 集合框架中的一个类,它实现了 Queue
接口,用于表示一个优先队列。优先队列是一种特殊的队列,它的元素按照优先级进行排序,每次从队列中取出的元素都是优先级最高(或最低)的元素。在 PriorityQueue
中,默认情况下,元素是按照自然顺序进行排序的,即元素需要实现 Comparable
接口;也可以通过传入一个 Comparator
来指定元素的排序规则。
2.2 继承关系与接口实现
下面是 PriorityQueue
类的定义以及它的继承关系和接口实现:
java
// PriorityQueue 类继承自 AbstractQueue 类,AbstractQueue 是一个抽象类,实现了 Queue 接口的部分方法
// 同时,PriorityQueue 实现了 Serializable 接口,表明它可以被序列化
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
// 类的具体实现将在后续详细分析
}
从上述代码可以看出,PriorityQueue
类继承自 AbstractQueue
类,这意味着它继承了 AbstractQueue
类中实现的 Queue
接口的部分方法。同时,PriorityQueue
实现了 Serializable
接口,这使得它可以被序列化,方便在网络传输或文件存储中使用。
2.3 与其他队列的对比
在 Java 中,还有其他一些队列实现,如 LinkedList
、ArrayDeque
等,它们与 PriorityQueue
的主要区别在于元素的排序和取出规则:
LinkedList
:是一个双向链表实现的队列,元素按照插入的顺序进行存储和取出,不考虑元素的优先级。ArrayDeque
:是一个基于数组实现的双端队列,同样元素按照插入的顺序进行存储和取出,不考虑元素的优先级。PriorityQueue
:元素按照优先级进行排序,每次取出的元素都是优先级最高(或最低)的元素。
三、PriorityQueue 的内部结构
3.1 核心属性
PriorityQueue
类有几个核心属性,用于存储元素和管理队列的状态。以下是这些核心属性的源码及注释:
java
// 默认的初始容量,当创建 PriorityQueue 时,如果没有指定初始容量,将使用这个值
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 用于存储队列元素的数组,是一个 Object 类型的数组,因为它可以存储任意类型的元素
transient Object[] queue;
// 队列中当前元素的数量
private int size = 0;
// 用于比较元素的比较器,如果为 null,则使用元素的自然顺序进行比较
private final Comparator<? super E> comparator;
// 记录队列结构修改的次数,用于迭代器的并发修改检查
transient int modCount = 0;
DEFAULT_INITIAL_CAPACITY
:默认的初始容量,当创建PriorityQueue
时,如果没有指定初始容量,将使用这个值。queue
:用于存储队列元素的数组,是一个Object
类型的数组,因为它可以存储任意类型的元素。size
:队列中当前元素的数量。comparator
:用于比较元素的比较器,如果为null
,则使用元素的自然顺序进行比较。modCount
:记录队列结构修改的次数,用于迭代器的并发修改检查。
3.2 构造函数
PriorityQueue
类提供了多个构造函数,用于创建不同初始状态的优先队列。以下是几个主要构造函数的源码及注释:
java
// 默认构造函数,创建一个初始容量为 11 的优先队列,使用元素的自然顺序进行比较
public PriorityQueue() {
// 调用另一个构造函数,传入默认初始容量和 null 比较器
this(DEFAULT_INITIAL_CAPACITY, null);
}
// 创建一个指定初始容量的优先队列,使用元素的自然顺序进行比较
public PriorityQueue(int initialCapacity) {
// 调用另一个构造函数,传入指定初始容量和 null 比较器
this(initialCapacity, null);
}
// 创建一个指定初始容量的优先队列,使用指定的比较器进行元素比较
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// 初始容量不能小于 1,否则抛出 IllegalArgumentException 异常
if (initialCapacity < 1)
throw new IllegalArgumentException();
// 初始化存储元素的数组
this.queue = new Object[initialCapacity];
// 初始化比较器
this.comparator = comparator;
}
// 创建一个包含指定集合元素的优先队列,使用元素的自然顺序进行比较
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
// 如果集合是 SortedSet 类型,获取其比较器
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
// 调用 initElementsFromCollection 方法初始化元素
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) {
// 如果集合是 PriorityQueue 类型,获取其比较器
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
// 调用 initFromPriorityQueue 方法初始化元素
initFromPriorityQueue(pq);
}
else {
// 否则,使用元素的自然顺序进行比较
this.comparator = null;
// 调用 initFromCollection 方法初始化元素
initFromCollection(c);
}
}
// 创建一个包含指定优先队列元素的优先队列,使用指定优先队列的比较器进行元素比较
public PriorityQueue(PriorityQueue<? extends E> c) {
// 获取指定优先队列的比较器
this.comparator = (Comparator<? super E>) c.comparator();
// 调用 initFromPriorityQueue 方法初始化元素
initFromPriorityQueue(c);
}
// 创建一个包含指定有序集合元素的优先队列,使用指定有序集合的比较器进行元素比较
public PriorityQueue(SortedSet<? extends E> c) {
// 获取指定有序集合的比较器
this.comparator = (Comparator<? super E>) c.comparator();
// 调用 initElementsFromCollection 方法初始化元素
initElementsFromCollection(c);
}
这些构造函数提供了多种方式来创建 PriorityQueue
,可以根据不同的需求选择合适的构造函数。例如,如果需要创建一个默认初始容量且使用元素自然顺序的优先队列,可以使用无参构造函数;如果需要指定初始容量和比较器,可以使用相应的构造函数。
3.3 堆结构
PriorityQueue
内部使用堆(通常是最小堆)来实现优先队列的功能。堆是一种完全二叉树,它满足堆的性质:对于最小堆,每个节点的值都小于或等于其子节点的值。在 PriorityQueue
中,堆是通过数组来实现的,数组的索引和二叉树的节点之间有如下对应关系:
- 对于数组中索引为
i
的节点,其左子节点的索引为2 * i + 1
。 - 其右子节点的索引为
2 * i + 2
。 - 其父节点的索引为
(i - 1) / 2
。
以下是一个简单的示例,展示了堆的数组表示和节点之间的关系:
java
// 假设我们有一个堆数组
int[] heap = {1, 3, 5, 7, 9, 11, 13};
// 根节点的索引为 0
int rootIndex = 0;
// 根节点的左子节点索引
int leftChildIndex = 2 * rootIndex + 1;
// 根节点的右子节点索引
int rightChildIndex = 2 * rootIndex + 2;
// 根节点的父节点索引(根节点没有父节点,这里只是演示计算方式)
int parentIndex = (rootIndex - 1) / 2;
System.out.println("Root value: " + heap[rootIndex]);
System.out.println("Left child value: " + heap[leftChildIndex]);
System.out.println("Right child value: " + heap[rightChildIndex]);
System.out.println("Parent value: " + (parentIndex >= 0 ? heap[parentIndex] : "None"));
通过这种数组表示方式,PriorityQueue
可以高效地实现堆的插入、删除和查找操作。
四、基本操作的源码分析
4.1 插入操作(offer)
4.1.1 offer(E e) 方法
offer(E e)
方法用于将一个元素插入到优先队列中。以下是该方法的源码及注释:
java
// 将指定元素插入到优先队列中
public boolean offer(E e) {
// 如果插入的元素为 null,抛出 NullPointerException 异常
if (e == null)
throw new NullPointerException();
// 记录队列结构修改的次数
modCount++;
// 获取当前队列中元素的数量
int i = size;
// 如果当前元素数量大于等于数组的长度,需要进行扩容
if (i >= queue.length)
grow(i + 1);
// 元素数量加 1
size = i + 1;
// 如果队列中没有元素,将元素直接插入到数组的第一个位置
if (i == 0)
queue[0] = e;
else
// 否则,将元素插入到合适的位置,维护堆的性质
siftUp(i, e);
return true;
}
- 首先,检查插入的元素是否为
null
,如果为null
,抛出NullPointerException
异常。 - 然后,记录队列结构修改的次数,以便在迭代器中进行并发修改检查。
- 接着,获取当前队列中元素的数量
i
。 - 如果
i
大于等于数组的长度,调用grow
方法进行扩容。 - 元素数量加 1。
- 如果队列中没有元素,将元素直接插入到数组的第一个位置。
- 否则,调用
siftUp
方法将元素插入到合适的位置,维护堆的性质。
4.1.2 grow(int minCapacity) 方法
grow(int minCapacity)
方法用于对存储元素的数组进行扩容。以下是该方法的源码及注释:
java
// 对存储元素的数组进行扩容
private void grow(int minCapacity) {
// 获取当前数组的长度
int oldCapacity = queue.length;
// 如果当前数组长度小于 64,新容量为旧容量的 2 倍 + 2
// 否则,新容量为旧容量的 1.5 倍
int newCapacity = oldCapacity + ((oldCapacity < 64)?
(oldCapacity + 2) :
(oldCapacity >> 1));
// 如果新容量超过了数组的最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 调用 hugeCapacity 方法处理超大容量的情况
newCapacity = hugeCapacity(minCapacity);
// 将旧数组复制到新数组中
queue = Arrays.copyOf(queue, newCapacity);
}
// 数组的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 处理超大容量的情况
private static int hugeCapacity(int minCapacity) {
// 如果最小容量小于 0,抛出 OutOfMemoryError 异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果最小容量超过最大数组容量,则返回 Integer.MAX_VALUE,否则返回最大数组容量
return (minCapacity > MAX_ARRAY_SIZE)?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 首先,获取当前数组的长度
oldCapacity
。 - 根据
oldCapacity
的大小计算新容量newCapacity
:如果oldCapacity
小于 64,新容量为旧容量的 2 倍 + 2;否则,新容量为旧容量的 1.5 倍。 - 如果新容量超过了数组的最大容量
MAX_ARRAY_SIZE
,调用hugeCapacity
方法处理超大容量的情况。 - 最后,使用
Arrays.copyOf
方法将旧数组复制到新数组中。
4.1.3 siftUp(int k, E x) 方法
siftUp(int k, E x)
方法用于将元素插入到合适的位置,维护堆的性质。以下是该方法的源码及注释:
java
// 将元素插入到合适的位置,维护堆的性质
private void siftUp(int k, E x) {
// 如果比较器不为 null,调用 siftUpUsingComparator 方法
if (comparator != null)
siftUpUsingComparator(k, x);
else
// 否则,调用 siftUpComparable 方法
siftUpComparable(k, x);
}
// 使用比较器将元素插入到合适的位置,维护堆的性质
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
// 从插入位置开始,向上调整堆
while (k > 0) {
// 计算父节点的索引
int parent = (k - 1) >>> 1;
// 获取父节点的值
Object e = queue[parent];
// 使用比较器比较插入元素和父节点的值
if (comparator.compare(x, (E) e) >= 0)
break;
// 如果插入元素小于父节点的值,将父节点的值下移
queue[k] = e;
// 更新插入位置为父节点的位置
k = parent;
}
// 将插入元素放到最终的位置
queue[k] = x;
}
// 使用元素的自然顺序将元素插入到合适的位置,维护堆的性质
@SuppressWarnings("unchecked")
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;
}
siftUp
方法根据比较器是否为null
,调用siftUpUsingComparator
或siftUpComparable
方法。siftUpUsingComparator
方法使用比较器比较插入元素和父节点的值,将插入元素向上调整到合适的位置。siftUpComparable
方法使用元素的自然顺序比较插入元素和父节点的值,将插入元素向上调整到合适的位置。
4.2 删除操作(poll)
4.2.1 poll() 方法
poll()
方法用于移除并返回优先队列中的最小元素。以下是该方法的源码及注释:
java
// 移除并返回优先队列中的最小元素
@SuppressWarnings("unchecked")
public E poll() {
// 如果队列中没有元素,返回 null
if (size == 0)
return null;
// 元素数量减 1
int s = --size;
// 记录队列结构修改的次数
modCount++;
// 获取队列中的第一个元素(即最小元素)
E result = (E) queue[0];
// 获取队列中的最后一个元素
E x = (E) queue[s];
// 将最后一个元素置为 null,帮助垃圾回收
queue[s] = null;
// 如果队列中还有元素,将最后一个元素放到根节点位置,然后向下调整堆
if (s != 0)
siftDown(0, x);
return result;
}
- 首先,检查队列中是否有元素,如果没有元素,返回
null
。 - 元素数量减 1。
- 记录队列结构修改的次数。
- 获取队列中的第一个元素(即最小元素)作为结果。
- 获取队列中的最后一个元素。
- 将最后一个元素置为
null
,帮助垃圾回收。 - 如果队列中还有元素,将最后一个元素放到根节点位置,然后调用
siftDown
方法向下调整堆。
4.2.2 siftDown(int k, E x) 方法
siftDown(int k, E x)
方法用于将元素向下调整到合适的位置,维护堆的性质。以下是该方法的源码及注释:
java
// 将元素向下调整到合适的位置,维护堆的性质
private void siftDown(int k, E x) {
// 如果比较器不为 null,调用 siftDownUsingComparator 方法
if (comparator != null)
siftDownUsingComparator(k, x);
else
// 否则,调用 siftDownComparable 方法
siftDownComparable(k, x);
}
// 使用比较器将元素向下调整到合适的位置,维护堆的性质
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
// 计算半元素数量,用于判断是否有子节点
int half = size >>> 1;
while (k < half) {
// 计算左子节点的索引
int child = (k << 1) + 1;
// 获取左子节点的值
Object c = queue[child];
// 计算右子节点的索引
int right = child + 1;
// 如果右子节点存在且右子节点的值小于左子节点的值
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
// 将右子节点作为较小的子节点
c = queue[child = right];
// 如果插入元素小于等于较小的子节点的值,停止调整
if (comparator.compare(x, (E) c) <= 0)
break;
// 否则,将较小的子节点的值上移
queue[k] = c;
// 更新调整位置为较小子节点的位置
k = child;
}
// 将插入元素放到最终的位置
queue[k] = x;
}
// 使用元素的自然顺序将元素向下调整到合适的位置,维护堆的性质
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
// 计算半元素数量,用于判断是否有子节点
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
// 计算左子节点的索引
int child = (k << 1) + 1; // assume left child is least
// 获取左子节点的值
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;
}
siftDown
方法根据比较器是否为null
,调用siftDownUsingComparator
或siftDownComparable
方法。siftDownUsingComparator
方法使用比较器比较插入元素和子节点的值,将插入元素向下调整到合适的位置。siftDownComparable
方法使用元素的自然顺序比较插入元素和子节点的值,将插入元素向下调整到合适的位置。
4.3 查看操作(peek)
4.3.1 peek() 方法
peek()
方法用于查看优先队列中的最小元素,但不移除该元素。以下是该方法的源码及注释:
java
// 查看优先队列中的最小元素,但不移除该元素
@SuppressWarnings("unchecked")
public E peek() {
// 如果队列中没有元素,返回 null
return (size == 0)? null : (E) queue[0];
}
- 该方法非常简单,只需检查队列中是否有元素,如果有元素,返回数组的第一个元素(即最小元素);如果没有元素,返回
null
。
4.4 判断队列是否为空(isEmpty)
4.4.1 isEmpty() 方法
isEmpty()
方法用于判断优先队列是否为空。以下是该方法的源码及注释:
java
// 判断优先队列是否为空
public boolean isEmpty() {
// 如果队列中元素数量为 0,返回 true,否则返回 false
return size == 0;
}
- 该方法通过比较队列中元素的数量是否为 0 来判断队列是否为空。
4.5 获取队列元素数量(size)
4.5.1 size() 方法
size()
方法用于获取优先队列中元素的数量。以下是该方法的源码及注释:
java
// 获取优先队列中元素的数量
public int size() {
// 返回队列中元素的数量
return size;
}
- 该方法直接返回
size
属性的值,即队列中元素的数量。
五、迭代器的实现
5.1 迭代器接口
PriorityQueue
类实现了 Iterable
接口,因此可以使用 iterator()
方法获取一个迭代器来遍历队列中的元素。以下是 iterator()
方法的源码及注释:
java
// 获取一个迭代器,用于遍历队列中的元素
public Iterator<E> iterator() {
// 返回一个 Itr 对象
return new Itr();
}
// 内部类 Itr 实现了 Iterator 接口,用于迭代 PriorityQueue 中的元素
private final class Itr implements Iterator<E> {
// 下一个要返回的元素的索引
private int cursor = 0;
// 最后一次返回的元素的索引
private int lastRet = -1;
// 记录创建迭代器时队列的修改次数,用于并发修改检查
private int expectedModCount = modCount;
// 用于存储被移除元素的堆
private ArrayDeque<E> forgetMeNot = null;
// 最后一次从 forgetMeNot 中取出的元素
private E lastRetElt = null;
// 判断是否还有下一个元素
public boolean hasNext() {
// 如果 cursor 小于队列中元素的数量,或者 forgetMeNot 不为空,返回 true,否则返回 false
return cursor < size ||
(forgetMeNot != null &&!forgetMeNot.isEmpty());
}
// 获取下一个元素
@SuppressWarnings("unchecked")
public E next() {
// 检查是否有并发修改
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
// 如果 cursor 小于队列中元素的数量
if (cursor < size)
// 返回当前 cursor 位置的元素,并更新 lastRet 和 cursor
return (E) queue[lastRet = cursor++];
// 如果 forgetMeNot 不为空
if (forgetMeNot != null) {
// 从 forgetMeNot 中取出元素
lastRet = -1;
lastRetElt = forgetMeNot.poll();
if (lastRetElt != null)
return lastRetElt;
}
// 如果没有下一个元素,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
}
// 删除当前迭代的元素
public void remove() {
// 如果 lastRet 为 -1,抛出 IllegalStateException 异常
if (lastRet < 0)
throw new IllegalStateException();
// 检查是否有并发修改
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
// 如果 lastRet 小于队列中元素的数量
if (lastRet < size) {
// 将队列中最后一个元素移到 lastRet 位置,然后向下调整堆
removeAt(lastRet);
}
// 如果 lastRetElt 不为空
else if (lastRetElt != null) {
// 如果 forgetMeNot 为空,创建一个 ArrayDeque
if (forgetMeNot == null) {
forgetMeNot = new ArrayDeque<>();
}
// 将 lastRetElt 从 forgetMeNot 中移除
forgetMeNot.remove(lastRetElt);
lastRetElt = null;
}
else {
// 如果 lastRet 大于等于队列中元素的数量且 lastRetElt 为空,抛出 IllegalStateException 异常
throw new IllegalStateException();
}
// 更新 expectedModCount
expectedModCount = modCount;
// 将 lastRet 置为 -1
lastRet = -1;
}
}
iterator()
方法返回一个Itr
对象,用于迭代PriorityQueue
中的元素。Itr
类实现了Iterator
接口,提供了以下方法:hasNext()
:判断是否还有下一个元素。next()
:获取下一个元素。在获取元素之前,会检查是否有并发修改,如果有则抛出ConcurrentModificationException
异常。remove()
:删除当前迭代的元素。在删除元素之前,会检查是否有并发修改,如果有则抛出ConcurrentModificationException
异常。
5.2 迭代顺序
需要注意的是,PriorityQueue
的迭代器并不保证按照元素的优先级顺序进行迭代。迭代器只是按照数组的顺序依次返回元素,而不是按照元素的优先级顺序。如果需要按照元素的优先级顺序遍历队列中的元素,可以使用 poll
方法依次取出元素。以下是一个示例代码:
java
import java.util.PriorityQueue;
import java.util.Iterator;
public class PriorityQueueIterationExample {
public static void main(String[] args) {
// 创建一个 PriorityQueue 对象
PriorityQueue<Integer> pq = new PriorityQueue<>();
// 向队列中添加元素
pq.offer(3);
pq.offer(1);
pq.offer(2);
// 使用迭代器遍历队列中的元素
System.out.println("Iteration using iterator:");
Iterator<Integer> iterator = pq.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// 使用 poll 方法按照优先级顺序遍历队列中的元素
System.out.println("Iteration using poll:");
while (!pq.isEmpty()) {
System.out.println(pq.poll());
}
}
}
在上述示例代码中,首先创建一个 PriorityQueue
对象,并向队列中添加元素。然后使用迭代器遍历队列中的元素,迭代器并不保证按照元素的优先级顺序进行迭代。接着使用 poll
方法按照优先级顺序遍历队列中的元素,poll
方法会依次取出队列中的最小元素。
六、线程安全机制
6.1 非线程安全
PriorityQueue
是非线程安全的,这意味着在多线程环境下,如果多个线程同时对 PriorityQueue
进行插入、删除等操作,可能会导致数据不一致或抛出异常。例如,一个线程正在进行插入操作,而另一个线程同时进行删除操作,可能会破坏堆的结构,导致元素的优先级顺序混乱。
6.2 替代方案
如果需要在多线程环境下使用优先队列,可以使用 PriorityBlockingQueue
类。PriorityBlockingQueue
是 Java 并发包中的一个类,它实现了 BlockingQueue
接口,是线程安全的优先队列。以下是一个使用 PriorityBlockingQueue
的示例代码:
java
import java.util.concurrent.PriorityBlockingQueue;
public class PriorityBlockingQueueExample {
public static void main(String[] args) {
// 创建一个 PriorityBlockingQueue 对象
PriorityBlockingQueue<Integer> pbq = new PriorityBlockingQueue<>();
// 向队列中添加元素
pbq.offer(3);
pbq.offer(1);
pbq.offer(2);
// 从队列中取出元素
while (!pbq.isEmpty()) {
System.out.println(pbq.poll());
}
}
}
在上述示例代码中,创建了一个 PriorityBlockingQueue
对象,并向队列中添加元素。然后使用 poll
方法从队列中取出元素,PriorityBlockingQueue
会保证在多线程环境下操作的线程安全。
七、性能分析
7.1 时间复杂度分析
- 插入操作(offer) :插入操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是队列中元素的数量。这是因为插入元素后,需要将元素向上调整到合适的位置,维护堆的性质,而堆的高度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn)。
- 删除操作(poll) :删除操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是队列中元素的数量。这是因为删除元素后,需要将最后一个元素放到根节点位置,然后将该元素向下调整到合适的位置,维护堆的性质,而堆的高度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn)。
- 查看操作(peek) :查看操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),因为只需要返回数组的第一个元素。
- 判断队列是否为空(isEmpty) :判断队列是否为空的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),因为只需要比较队列中元素的数量是否为 0。
- 获取队列元素数量(size) :获取队列元素数量的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),因为只需要返回
size
属性的值。
7.2 空间复杂度分析
PriorityQueue
的空间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是队列中元素的数量。这是因为需要使用一个数组来存储队列中的元素,数组的长度至少为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n。
7.3 与其他数据结构的性能对比
- 与
ArrayList
的对比 :- 插入和删除操作 :
PriorityQueue
的插入和删除操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn),而ArrayList
的插入和删除操作在最坏情况下的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),因为需要移动大量元素。 - 查看操作 :
PriorityQueue
的查看操作(peek
)的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),而ArrayList
的随机访问操作的时间复杂度也为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。 - 排序 :
PriorityQueue
会自动维护元素的优先级顺序,而ArrayList
需要手动调用排序方法进行排序。
- 插入和删除操作 :
- 与
TreeSet
的对比 :- 插入和删除操作 :
PriorityQueue
的插入和删除操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn),TreeSet
的插入和删除操作的时间复杂度也为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn)。 - 元素唯一性 :
PriorityQueue
允许存储重复元素,而TreeSet
不允许存储重复元素。 - 迭代顺序 :
PriorityQueue
的迭代器不保证按照元素的优先级顺序进行迭代,而TreeSet
会按照元素的自然顺序或指定的比较器顺序进行迭代。
- 插入和删除操作 :
八、应用场景
8.1 任务调度
在任务调度系统中,不同的任务可能具有不同的优先级,需要按照优先级顺序执行任务。PriorityQueue
可以用于存储任务,每次从队列中取出优先级最高的任务进行执行。以下是一个简单的任务调度示例代码:
java
import java.util.PriorityQueue;
// 任务类,实现 Comparable 接口,根据优先级进行排序
class Task implements Comparable<Task> {
private int priority;
private String name;
public Task(int priority, String name) {
this.priority = priority;
this.name = name;
}
public int getPriority() {
return priority;
}
public String getName() {
return name;
}
@Override
public int compareTo(Task other) {
// 优先级高的任务排在前面
return Integer.compare(other.priority, this.priority);
}
@Override
public String toString() {
return "Task [priority=" + priority + ", name=" + name + "]";
}
}
public class TaskScheduler {
public static void main(String[] args) {
// 创建一个 PriorityQueue 对象,用于存储任务
PriorityQueue<Task> taskQueue = new PriorityQueue<>();
// 向队列中添加任务
taskQueue.offer(new Task(3, "Task 3"));
taskQueue.offer(new Task(1, "Task 1"));
taskQueue.offer(new Task(2, "Task 2"));
java
// 执行任务
while (!taskQueue.isEmpty()) {
Task task = taskQueue.poll();
System.out.println("Executing task: " + task);
}
}
}
在这个示例中,Task
类实现了 Comparable
接口,根据任务的优先级进行排序。PriorityQueue
会自动根据任务的优先级对任务进行排序,每次从队列中取出优先级最高的任务进行执行。这样就可以实现一个简单的任务调度系统,确保高优先级的任务优先执行。
8.2 图算法 - Dijkstra 算法
Dijkstra 算法是一种用于计算图中单个源节点到所有其他节点的最短路径的算法。在算法的执行过程中,需要不断地从优先队列中取出距离源节点最近的节点进行扩展。以下是使用 PriorityQueue
实现 Dijkstra 算法的示例代码:
java
import java.util.*;
// 图的节点类
class Node implements Comparable<Node> {
int vertex;
int distance;
public Node(int vertex, int distance) {
this.vertex = vertex;
this.distance = distance;
}
@Override
public int compareTo(Node other) {
// 距离小的节点排在前面
return Integer.compare(this.distance, other.distance);
}
}
// 图类
class Graph {
private int V; // 顶点数
private List<List<Node>> adj; // 邻接表
public Graph(int v) {
V = v;
adj = new ArrayList<>(v);
for (int i = 0; i < v; i++) {
adj.add(new ArrayList<>());
}
}
// 添加边
public void addEdge(int u, int v, int weight) {
adj.get(u).add(new Node(v, weight));
adj.get(v).add(new Node(u, weight)); // 无向图
}
// Dijkstra 算法
public int[] dijkstra(int src) {
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
PriorityQueue<Node> pq = new PriorityQueue<>();
pq.offer(new Node(src, 0));
while (!pq.isEmpty()) {
Node current = pq.poll();
int u = current.vertex;
int d = current.distance;
// 如果当前距离大于已记录的距离,跳过
if (d > dist[u]) {
continue;
}
for (Node neighbor : adj.get(u)) {
int v = neighbor.vertex;
int weight = neighbor.distance;
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.offer(new Node(v, dist[v]));
}
}
}
return dist;
}
}
public class DijkstraExample {
public static void main(String[] args) {
int V = 5;
Graph graph = new Graph(V);
graph.addEdge(0, 1, 4);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 2);
graph.addEdge(1, 3, 5);
graph.addEdge(2, 3, 8);
graph.addEdge(2, 4, 10);
graph.addEdge(3, 4, 2);
int src = 0;
int[] dist = graph.dijkstra(src);
for (int i = 0; i < V; i++) {
System.out.println("Shortest distance from " + src + " to " + i + " is " + dist[i]);
}
}
}
在这个示例中,Node
类表示图中的节点,实现了 Comparable
接口,根据节点到源节点的距离进行排序。Graph
类表示图,包含了添加边和执行 Dijkstra 算法的方法。在 Dijkstra 算法中,使用 PriorityQueue
存储待扩展的节点,每次从队列中取出距离源节点最近的节点进行扩展,更新其邻接节点的距离,并将更新后的节点加入优先队列中。这样可以确保每次扩展的节点都是距离源节点最近的节点,从而找到最短路径。
8.3 数据流中的中位数
在处理数据流时,需要动态地计算数据流中的中位数。可以使用两个优先队列来实现,一个最大堆(使用 PriorityQueue
并自定义比较器)存储较小的一半元素,一个最小堆存储较大的一半元素。以下是示例代码:
java
import java.util.PriorityQueue;
class MedianFinder {
// 最大堆,存储较小的一半元素
private PriorityQueue<Integer> maxHeap;
// 最小堆,存储较大的一半元素
private PriorityQueue<Integer> minHeap;
public MedianFinder() {
maxHeap = new PriorityQueue<>((a, b) -> b - a);
minHeap = new PriorityQueue<>();
}
// 添加元素
public void addNum(int num) {
if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.offer(num);
} else {
minHeap.offer(num);
}
// 平衡两个堆的大小
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.offer(maxHeap.poll());
} else if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
// 获取中位数
public double findMedian() {
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
} else {
return maxHeap.peek();
}
}
}
public class MedianFinderExample {
public static void main(String[] args) {
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1);
medianFinder.addNum(2);
System.out.println("Median: " + medianFinder.findMedian());
medianFinder.addNum(3);
System.out.println("Median: " + medianFinder.findMedian());
}
}
在这个示例中,MedianFinder
类包含两个优先队列:maxHeap
是一个最大堆,用于存储较小的一半元素;minHeap
是一个最小堆,用于存储较大的一半元素。每次添加元素时,根据元素的大小将其添加到合适的堆中,并平衡两个堆的大小,确保 maxHeap
的大小最多比 minHeap
的大小大 1。获取中位数时,根据两个堆的大小关系计算中位数。
九、常见问题及解决方案
9.1 元素不支持自然排序
如果要存储的元素没有实现 Comparable
接口,并且没有提供自定义的 Comparator
,在插入元素时会抛出 ClassCastException
异常。解决方案是让元素类实现 Comparable
接口,或者在创建 PriorityQueue
时提供一个自定义的 Comparator
。以下是一个示例:
java
import java.util.PriorityQueue;
import java.util.Comparator;
// 自定义类,没有实现 Comparable 接口
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class NonComparableElementExample {
public static void main(String[] args) {
// 创建一个自定义的 Comparator
Comparator<Person> ageComparator = (p1, p2) -> Integer.compare(p1.age, p2.age);
// 创建 PriorityQueue 并传入自定义的 Comparator
PriorityQueue<Person> pq = new PriorityQueue<>(ageComparator);
pq.offer(new Person("Alice", 25));
pq.offer(new Person("Bob", 20));
while (!pq.isEmpty()) {
System.out.println(pq.poll());
}
}
}
在这个示例中,Person
类没有实现 Comparable
接口,通过创建一个自定义的 Comparator
并在创建 PriorityQueue
时传入该 Comparator
,可以正确地对 Person
对象进行排序。
9.2 并发修改问题
由于 PriorityQueue
是非线程安全的,在多线程环境下进行并发修改可能会导致数据不一致或抛出异常。解决方案是使用线程安全的优先队列 PriorityBlockingQueue
,或者在多线程访问 PriorityQueue
时进行同步操作。以下是使用 PriorityBlockingQueue
的示例:
java
import java.util.concurrent.PriorityBlockingQueue;
class Task implements Comparable<Task> {
private int priority;
private String name;
public Task(int priority, String name) {
this.priority = priority;
this.name = name;
}
public int getPriority() {
return priority;
}
public String getName() {
return name;
}
@Override
public int compareTo(Task other) {
return Integer.compare(other.priority, this.priority);
}
@Override
public String toString() {
return "Task [priority=" + priority + ", name=" + name + "]";
}
}
public class ConcurrentPriorityQueueExample {
public static void main(String[] args) {
// 创建一个 PriorityBlockingQueue 对象
PriorityBlockingQueue<Task> pq = new PriorityBlockingQueue<>();
// 模拟多线程添加任务
Thread producer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
pq.offer(new Task(i, "Task " + i));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 模拟多线程取出任务
Thread consumer = new Thread(() -> {
while (true) {
try {
Task task = pq.take();
System.out.println("Processing task: " + task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
consumer.start();
}
}
在这个示例中,使用 PriorityBlockingQueue
来处理多线程环境下的任务调度。producer
线程负责向队列中添加任务,consumer
线程负责从队列中取出任务进行处理。PriorityBlockingQueue
会自动处理线程安全问题,确保数据的一致性。
9.3 性能问题
在某些情况下,PriorityQueue
的性能可能无法满足需求。例如,当插入和删除操作非常频繁时,由于每次操作都需要维护堆的性质,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn),可能会导致性能下降。解决方案是根据具体场景选择合适的数据结构。如果对插入和删除操作的性能要求较高,并且不需要严格的优先级排序,可以考虑使用其他数据结构,如 ArrayList
或 LinkedList
;如果需要在多线程环境下使用,并且对性能有较高要求,可以考虑使用并发数据结构。
十、总结与展望
10.1 总结
Java PriorityQueue
是一个非常实用的数据结构,它基于堆实现了优先队列的功能。通过使用 PriorityQueue
,可以方便地对元素进行优先级排序,每次取出的元素都是优先级最高(或最低)的元素。
在内部结构上,PriorityQueue
使用数组来存储元素,通过堆的性质来维护元素的优先级顺序。其核心操作如插入、删除和查看等,都具有较好的时间复杂度。插入和删除操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(log n) </math>O(logn),查看操作的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。
PriorityQueue
提供了多种构造函数,可以根据不同的需求创建不同初始状态的优先队列。同时,它还实现了 Iterable
接口,支持使用迭代器遍历队列中的元素,但迭代器不保证按照元素的优先级顺序进行迭代。
需要注意的是,PriorityQueue
是非线程安全的,在多线程环境下使用时需要进行同步操作或使用线程安全的替代方案 PriorityBlockingQueue
。
10.2 展望
随着 Java 技术的不断发展,PriorityQueue
可能会在以下方面得到进一步的优化和扩展:
- 性能优化 :在未来的 Java 版本中,可能会对
PriorityQueue
的实现进行优化,减少插入和删除操作的时间复杂度,提高性能。例如,采用更高效的堆结构或算法来维护元素的优先级顺序。 - 并发支持 :虽然已经有了
PriorityBlockingQueue
作为线程安全的优先队列实现,但在某些特定的并发场景下,可能还需要更高效的并发优先队列。未来可能会引入新的并发数据结构,以满足不同的并发需求。 - 功能扩展 :可能会为
PriorityQueue
增加更多的功能,如支持批量插入和删除操作、支持元素的更新操作等,以提高其使用的灵活性。 - 与其他数据结构的集成 :
PriorityQueue
可能会与其他数据结构进行更紧密的集成,例如与图算法、排序算法等结合,提供更强大的功能。
总之,PriorityQueue
作为 Java 集合框架中的重要组成部分,在未来的发展中有望为开发者提供更高效、更强大的功能,帮助开发者更好地解决各种实际问题。同时,开发者也应该根据具体的需求,合理选择和使用 PriorityQueue
以及其他相关的数据结构,以提高程序的性能和可维护性。
通过对 Java PriorityQueue
的深入分析,我们不仅了解了其使用原理和实现细节,还掌握了其在不同场景下的应用方法。在实际开发中,我们可以根据具体的需求,灵活运用 PriorityQueue
来解决各种问题,提高程序的效率和质量。希望本文能够对读者有所帮助,让大家在 Java 编程中更加得心应手地使用 PriorityQueue
。