本文分享自華爲雲社區《滾雪球學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()方法還是比較關鍵的,它是用來上浮元素,維護小根堆的性質的。
刪除元素
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(); // 轉發數據 } } }
拓展:
上面是一個簡單的網絡代理程序,其中包含兩個類:IpPacket
和MyNetworkProxy
。
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接口,因此可以像隊列一樣進行添加和刪除元素,並且堆的性質保證了每次返回的元素都是最優的。
如下是部分源碼截圖:
具體分析如下:
-
PriorityQueue類是一個泛型類,使用類名後面的<E>表示。
-
類中定義了一個序列化ID,一個底層數組用於存儲堆元素,一個記錄數組中元素數量的變量,一個記錄堆發生修改次數的變量,以及一個比較器。其中比較器用於判斷堆中元素的順序。
-
類中定義了多個構造方法,可以創建一個初始容量爲11的PriorityQueue,也可以指定初始容量和比較器。
-
PriorityQueue類實現了Queue接口中的offer、peek和poll方法。其中offer方法用於將元素添加到PriorityQueue中,peek方法用於返回PriorityQueue的頭部元素,如果隊列爲空,則返回null,poll方法用於刪除PriorityQueue的頭部元素,並返回該元素,如果隊列爲空,則返回null。
-
類中還定義了一個size方法,用於返回PriorityQueue中元素的數量。
-
PriorityQueue類實現了Iterable接口,因此可以迭代PriorityQueue中的元素。在此基礎上,類中定義了一個Itr迭代器類,用於遍歷PriorityQueue中的元素。其中,由於堆不保證元素的順序,因此元素的順序是不確定的。
-
類中還實現了一個使用指定的Collection構造PriorityQueue的方法,用於將一個Collection中的元素添加到PriorityQueue中。
-
heapify方法用於對元素進行排序,排序的規則由比較器決定。
-
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時,需要根據實際情況選擇適合的數據結構和算法來解決問題。