PriorityQueue 源码深度分析(Java 8)

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

如果传入的集合是 SortedSetPriorityQueue,直接复用其比较器;否则调用 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] 堆化过程:

  1. 最后一个非叶子节点索引 (5/2)-1 = 1(元素 3),执行 siftDown
  2. 再处理索引 0(元素 5),执行 siftDown
  3. 最终得到小顶堆 [1,2,5,3,4]

三、核心操作:入队、出队与堆维护

PriorityQueue 的核心是入队(offer)时上浮(siftUp出队(poll)时下沉(siftDown,通过这两个操作保证堆的性质。

1. 入队(offer(E e)):添加元素并上浮

PriorityQueue 是无界队列,offeradd 等价(均不抛容量不足异常),核心步骤:

  1. 检查元素非 null(不允许 null);
  2. 扩容(若 size >= 数组容量);
  3. 元素添加到数组末尾(size 位置);
  4. 上浮(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()):移除堆顶并下沉

核心步骤:

  1. 空队列返回 null;
  2. 取堆顶元素(elementData[0],优先级最高);
  3. 将数组末尾元素移到堆顶(elementData[0] = elementData[--size]);
  4. 下沉(siftDown)调整堆;
  5. 返回原堆顶元素。
源码实现
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_SIZEInteger.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):移除指定元素

步骤:

  1. 遍历数组找到元素索引 i
  2. 用末尾元素替换索引 i 的元素;
  3. 根据元素位置判断是上浮(siftUp)还是下沉(siftDown);
  4. 释放末尾元素引用。
源码核心逻辑
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) 堆调整

六、典型应用场景

  1. 任务调度:按任务优先级执行(例:线程池的延迟队列 DelayQueue 基于 PriorityQueue 实现);
  2. TopK 问题:找最大 / 最小的 k 个元素(例:用小顶堆找最大 k 个元素,时间复杂度 O (n log k));
  3. 最短路径算法:Dijkstra 算法中,用优先级队列选择当前最短路径节点。

总结

PriorityQueue 的核心是基于数组的二叉小顶堆,通过 siftUp(入队)和 siftDown(出队)维护堆的性质,保证优先级最高的元素始终在堆顶。其设计权衡了入队 / 出队的高效性(O (log n)),但牺牲了查找和遍历的效率(O (n)),适用于需要按优先级处理元素的场景。

关键记住:数组存储堆 + 上浮下沉调堆 + 动态扩容 是 PriorityQueue 的核心实现逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值