深入分析Java中的PriorityQueue底層實現與原始碼

华为云开发者联盟發表於2024-03-19

本文分享自華為雲社群《滾雪球學Java(70):深入理解Java中的PriorityQueue底層實現與原始碼分析》,作者: bug菌。

環境說明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

@[toc]

前言

PriorityQueue是Java中一個非常常用的資料結構,它可以實現基於優先順序的排序,常用於任務排程、事件處理等場景。本文將深入探討Java中PriorityQueue的底層實現與原始碼分析,幫助讀者更好地理解PriorityQueue的內部原理。

摘要

本文將從PriorityQueue的定義、特性入手,逐步分析其底層實現、原始碼解析以及應用場景案例、優缺點分析等方面,全面深入地理解PriorityQueue。

PriorityQueue

概述

PriorityQueue的定義與特性

在Java中,PriorityQueue是一個優先順序佇列,它是基於陣列實現的,但是其中的元素不是按照插入順序排列,而是按照元素的優先順序進行排序。你可以將任意型別的物件插入PriorityQueue中,並且PriorityQueue會按照元素的自然順序或者你自己定義的優先順序順序進行排序。

PriorityQueue是一個無界佇列,即佇列的容量可以無限擴充。它是執行緒不安全的,不支援null元素。預設情況下,PriorityQueue是自然排序,也就是小根堆,也可以透過Comparator介面來指定元素的排序方式。

PriorityQueue的底層實現

PriorityQueue是基於陣列實現的,它的底層資料結構是一個小根堆。小根堆是一種完全二叉樹,滿足一個性質,即每個節點的值都小於或等於它的左右子節點的值。

在PriorityQueue中,陣列的第一個元素是堆頂,也就是優先順序最高的元素。每次插入一個元素時,PriorityQueue會先將元素新增到陣列末尾,然後透過上浮操作來維護小根堆的性質。每次刪除堆頂元素時,PriorityQueue會先將陣列末尾元素移動到堆頂,然後透過下沉操作來維護小根堆的性質。

原始碼解析

PriorityQueue的建構函式

    public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

    public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }

    public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }

    public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }

在這裡插入圖片描述

PriorityQueue有四個建構函式:預設建構函式、指定初始化容量的建構函式、指定Comparator的建構函式和同時指定初始化容量與Comparator的建構函式。

當不指定初始化容量和Comparator時,將會使用預設值。預設容量為11,Comparator為null。

如下是部分原始碼截圖:

新增元素

    public boolean add(E e) {
        return offer(e);
    }

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        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;
    }

    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

新增元素時,會先判斷佇列是否已滿,如果已滿則透過grow()方法進行擴容。接著會將元素新增到陣列末尾,然後透過siftUp()方法來維護小根堆的性質。

如果指定了Comparator,則透過siftUpUsingComparator()方法來維護小根堆的性質;否則透過siftUpComparable()方法維護小根堆的性質。這裡的siftUp()方法還是比較關鍵的,它是用來上浮元素,維護小根堆的性質的。

深入分析Java中的PriorityQueue底層實現與原始碼

刪除元素

    public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        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);
    }

刪除元素時,會先判斷佇列是否為空。如果不為空,就將堆頂元素取出作為返回值,然後將陣列末尾的元素移動到堆頂,透過siftDown()方法來維護小根堆的性質。

如果指定了Comparator,則透過siftDownUsingComparator()方法來維護小根堆的性質;否則透過siftDownComparable()方法維護小根堆的性質。

擴充:

這段程式碼是Java中PriorityQueue類中的poll()方法的實現。poll()方法用於從佇列中取出並刪除佇列頭部的元素,如果佇列為空則返回null。

該方法首先檢查佇列是否為空,如果為空則返回null。否則,將佇列中的元素個數減1,更新modCount屬性表示這次操作改變了佇列結構,將佇列頭部的元素用變數result儲存。接著,將佇列中最後一個元素用變數x儲存,並將佇列中最後一個元素置為null。若佇列還有元素,呼叫siftDown()方法將變數x與佇列中元素重新排序,確保佇列滿足小根堆的性質。最後,返回變數result,即佇列中被刪除的元素。

在siftDown()方法中,首先判斷comparator是否為null。如果不為null,則呼叫siftDownUsingComparator()方法,否則呼叫siftDownComparable()方法。這兩個方法都是用來重建小根堆的,不同的是,siftDownUsingComparator()方法透過比較器來實現排序,而siftDownComparable()方法則透過元素的自然順序來實現排序。

應用場景案例

任務排程

假設我們需要實現一個任務排程器,能夠按照任務的優先順序來執行任務。我們可以將任務按照優先順序新增到PriorityQueue中,然後按照佇列中任務的順序依次執行。

public class Task implements Comparable<Task> {

    private int priority;
    private Runnable runnable;

    public Task(int priority, Runnable runnable) {
        this.priority = priority;
        this.runnable = runnable;
    }

    public void run() {
        runnable.run();
    }

    @Override
    public int compareTo(Task o) {
        return Integer.compare(priority, o.priority);
    }

}

public class MyTaskScheduler {

    private PriorityQueue<Task> queue;

    public MyTaskScheduler() {
        queue = new PriorityQueue<>();
    }

    public void schedule(Task task) {
        queue.offer(task);
    }

    public void run() {
        while (!queue.isEmpty()) {
            Task task = queue.poll();
            task.run();
        }
    }

}

分析程式碼:

這段程式碼定義了一個Task類和一個MyTaskScheduler類,用於實現任務排程。Task類包含了一個優先順序和一個Runnable物件,用於儲存待執行的任務和它的優先順序。MyTaskScheduler類包含了一個優先佇列,用於儲存所有的任務,並且包括了兩個方法:schedule()方法用於將任務加入到佇列中,run()方法則用於執行佇列中的所有任務。

Task類實現了Comparable介面,重寫了compareTo()方法,透過比較任務的優先順序,來判斷哪個任務先執行。MyTaskScheduler類使用了Java自帶的PriorityQueue優先佇列,保證了任務的執行順序是按照優先順序從高到低的順序執行。在run()方法中,使用一個while迴圈,不斷從佇列中取出優先順序最高的任務,直到佇列為空。對於每個任務,呼叫它的run()方法來執行任務邏輯。

這段程式碼可以用於實現多個任務的排程,透過設定不同的優先順序,來控制不同任務的執行順序。同時,使用優先佇列可以保證任務按照優先順序的順序執行,提高了任務執行的效率。

網路代理

假設我們需要實現一個網路代理,能夠按照ip包的優先順序來轉發資料。我們可以將ip包按照優先順序新增到PriorityQueue中,然後按照佇列中ip包的順序依次轉發資料。

public class IpPacket implements Comparable<IpPacket> {

    private int priority;
    private byte[] data;

    public IpPacket(int priority, byte[] data) {
        this.priority = priority;
        this.data = data;
    }

    public byte[] getData() {
        return data;
    }

    @Override
    public int compareTo(IpPacket o) {
        return Integer.compare(priority, o.priority);
    }

}

public class MyNetworkProxy {

    private PriorityQueue<IpPacket> queue;

    public MyNetworkProxy() {
        queue = new PriorityQueue<>();
    }

    public void send(IpPacket packet) {
        queue.offer(packet);
    }

    public void receive() {
        while (!queue.isEmpty()) {
            IpPacket packet = queue.poll();
            // 轉發資料
        }
    }

}

擴充:

上面是一個簡單的網路代理程式,其中包含兩個類:IpPacketMyNetworkProxy

IpPacket類表示一個IP資料包,包含資料的優先順序和實際的資料。實現了Comparable介面,用於優先順序的排序。

MyNetworkProxy類表示一個網路代理,透過維護一個優先順序佇列來實現資料包的轉發。send方法用於將資料包新增到佇列中,receive方法用於從佇列中取出優先順序最高的資料包並進行轉發。

MyNetworkProxy中,當佇列不為空時,每次從佇列頭取出優先順序最高的資料包進行轉發,直到佇列為空。

這個程式中的優先順序佇列可以保證高優先順序的資料包先被髮送,以保證網路的效率和響應時間。

優缺點分析

優點

  • PriorityQueue是一個非常高效的資料結構,它的插入和刪除元素的時間複雜度都是O(log n)。
  • PriorityQueue可以實現基於優先順序的排序,適用於任務排程、事件處理等場景。
  • PriorityQueue在擴容時可以節省空間,只需將陣列容量增加一半即可。
  • PriorityQueue支援自然排序和指定Comparator來定義元素的排序方式。

缺點

  • PriorityQueue是執行緒不安全的,不適合在多執行緒環境下使用。如果需要在多執行緒環境下使用,可以使用PriorityBlockingQueue。
  • PriorityQueue不支援隨機訪問元素,只能訪問堆頂元素。如果需要隨機訪問元素,可以使用TreeSet。

類程式碼方法介紹

public

public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {

    /**
     * 序列化 ID
     */
    private static final long serialVersionUID = -7720805057305804111L;

    /**
     * 預設初始容量
     */
    private static final int DEFAULT_INITIAL_CAPACITY = 11;

    /**
     * 底層陣列,用於儲存堆元素
     */
    transient Object[] queue;

    /**
     * 陣列中元素的數量
     */
    private int size = 0;

    /**
     * 比較器,用於確定堆中元素的順序
     */
    private final Comparator<? super E> comparator;

    /**
     * 修改次數,用於判斷在迭代過程中堆是否發生了修改
     */
    transient int modCount = 0;

    /**
     * 構造一個初始容量為11的PriorityQueue,元素順序按照自然順序排列
     */
    public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

    /**
     * 構造一個指定初始容量的PriorityQueue,元素順序按照自然順序排列
     */
    public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }

    /**
     * 構造一個初始容量為11的PriorityQueue,元素順序按照指定比較器排列
     */
    public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }

    /**
     * 返回PriorityQueue的頭部元素,如果佇列為空,則返回null
     */
    public E peek() {
        if (size == 0) {
            return null;
        }
        return (E) queue[0];
    }

    /**
     * 返回PriorityQueue中元素的數量
     */
    public int size() {
        return size;
    }

    /**
     * 對元素進行排序,排序的規則由比較器決定
     */
    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--) {
            siftDown(i, (E) queue[i]);
        }
    }

    /**
     * 返回一個包含PriorityQueue中所有元素的陣列
     */
    @Override
    public Object[] toArray() {
        return Arrays.copyOf(queue, size);
    }

以上是Java中PriorityQueue類的部分原始碼。PriorityQueue是一個堆,可以儲存任意型別的元素,但是需要這些元素是可比較的,可以按照一定的順序進行排序。PriorityQueue實現了Queue介面,因此可以像佇列一樣進行新增和刪除元素,並且堆的性質保證了每次返回的元素都是最優的。

如下是部分原始碼截圖:

在這裡插入圖片描述

具體分析如下:

  1. PriorityQueue類是一個泛型類,使用類名後面的<E>表示。

  2. 類中定義了一個序列化ID,一個底層陣列用於儲存堆元素,一個記錄陣列中元素數量的變數,一個記錄堆發生修改次數的變數,以及一個比較器。其中比較器用於判斷堆中元素的順序。

  3. 類中定義了多個構造方法,可以建立一個初始容量為11的PriorityQueue,也可以指定初始容量和比較器。

  4. PriorityQueue類實現了Queue介面中的offer、peek和poll方法。其中offer方法用於將元素新增到PriorityQueue中,peek方法用於返回PriorityQueue的頭部元素,如果佇列為空,則返回null,poll方法用於刪除PriorityQueue的頭部元素,並返回該元素,如果佇列為空,則返回null。

  5. 類中還定義了一個size方法,用於返回PriorityQueue中元素的數量。

  6. PriorityQueue類實現了Iterable介面,因此可以迭代PriorityQueue中的元素。在此基礎上,類中定義了一個Itr迭代器類,用於遍歷PriorityQueue中的元素。其中,由於堆不保證元素的順序,因此元素的順序是不確定的。

  7. 類中還實現了一個使用指定的Collection構造PriorityQueue的方法,用於將一個Collection中的元素新增到PriorityQueue中。

  8. heapify方法用於對元素進行排序,排序的規則由比較器決定。

  9. toArray方法用於返回一個包含PriorityQueue中所有元素的陣列。其中,使用Arrays類的copyOf方法對底層陣列進行復制和擷取,以保證只返回PriorityQueue中的有效元素。

測試用例

下面是一個簡單的示例main函式,使用Java中的PriorityQueue實現一個整數優先順序佇列,並新增一些元素並列印結果:

測試程式碼演示

package com.demo.javase.day70;

import java.util.PriorityQueue;

/**
 * @Author bug菌
 * @Date 2023-11-06 16:08
 */
public class PriorityQueueTest {

    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        pq.add(5);
        pq.add(1);
        pq.add(10);
        pq.add(3);
        pq.add(2);

        System.out.println("佇列中的元素(從小到大):");
        while (!pq.isEmpty()) {
            System.out.print(pq.poll() + " ");
        }
    }
}

測試結果

根據如上測試用例,本地測試結果如下,僅供參考,你們也可以自行修改測試用例或者新增更多的測試資料或測試方法,進行熟練學習以此加深理解。

輸出結果:

佇列中的元素(從小到大):
1 2 3 5 10

在這個示例中,我們建立了一個PriorityQueue物件pq,並新增了5個整數元素。然後,我們使用poll()方法按照優先順序順序逐個彈出元素,並列印結果。注意,PriorityQueue預設使用自然順序(從小到大),因此我們不需要指定比較器。

實際執行結果如下:

在這裡插入圖片描述

測試程式碼分析

根據如上測試用例,在此我給大家進行深入詳細的解讀一下測試程式碼,以便於更多的同學能夠理解並加深印象。

如上測試用例演示了Java中的PriorityQueue(優先佇列)的用法。在主方法中,先建立了一個PriorityQueue物件pq,並向其中新增了五個整數元素(5,1,10,3,2)。然後透過while迴圈,從佇列中取出元素並列印,因為PriorityQueue預設是小根堆,所以列印出來的元素是從小到大排列的。最終輸出結果為:佇列中的元素(從小到大):1 2 3 5 10

小結

本文透過對Java中PriorityQueue的定義、特性、底層實現及原始碼解析進行詳細分析,深入探討了PriorityQueue的內部原理。PriorityQueue是一種基於陣列實現的堆,它可以按照元素的優先順序進行排序,常用於任務排程、事件處理等場景。PriorityQueue底層是一個小根堆,在元素新增和刪除時,會透過上浮和下沉操作來維護小根堆的性質。同時,本文也介紹了PriorityQueue的應用場景案例以及優缺點分析,幫助讀者更好地理解PriorityQueue。

總之,PriorityQueue作為Java集合框架中的一個重要組成部分,對於Java開發者來說,是必不可少的知識點。讀者可以透過學習本文,加深對PriorityQueue的理解,從而更好地應用於實際開發中。

總結

本文從PriorityQueue的定義、特性和底層實現入手,深入剖析了Java中PriorityQueue的原始碼和應用場景案例,並對其進行了優缺點分析。PriorityQueue是一種非常高效的資料結構,可以實現基於優先順序的排序,適用於任務排程、事件處理等場景。但是需要注意的是,它是執行緒不安全的,不支援隨機訪問元素。在使用PriorityQueue時,需要根據實際情況選擇適合的資料結構和演算法來解決問題。

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章