詳解Java中PriorityQueue的作用和源碼實(shí)現(xiàn)
引言
前面文章我們講解了ArrayBlockingQueue
和LinkedBlockingQueue
源碼,這篇文章開(kāi)始講解PriorityQueue源碼。從名字上就能看到ArrayBlockingQueue
是基于數(shù)組實(shí)現(xiàn)的,而LinkedBlockingQueue
是基于鏈表實(shí)現(xiàn),而PriorityQueue
是基于什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的,看不出來(lái),好像是實(shí)現(xiàn)了優(yōu)先級(jí)的隊(duì)列。
由于PriorityQueue
跟前幾個(gè)阻塞隊(duì)列不一樣,并沒(méi)有實(shí)現(xiàn)BlockingQueue
接口,只是實(shí)現(xiàn)了Queue
接口,Queue
接口中定義了幾組放數(shù)據(jù)和取數(shù)據(jù)的方法,來(lái)滿足不同的場(chǎng)景。
操作 | 拋出異常 | 返回特定值 |
---|---|---|
放數(shù)據(jù) | add() | offer() |
取數(shù)據(jù)(同時(shí)刪除數(shù)據(jù)) | remove() | poll() |
查看數(shù)據(jù)(不刪除) | element() | peek() |
這兩組方法的區(qū)別是:
- 當(dāng)隊(duì)列滿的時(shí)候,再次添加數(shù)據(jù),add()會(huì)拋出異常,offer()會(huì)返回false。
- 當(dāng)隊(duì)列為空的時(shí)候,再次取數(shù)據(jù),remove()會(huì)拋出異常,poll()會(huì)返回null。
PriorityQueue
也會(huì)有針對(duì)這幾組放數(shù)據(jù)和取數(shù)據(jù)方法的具體實(shí)現(xiàn)。
類結(jié)構(gòu)
先看一下PriorityQueue
類里面有哪些屬性:
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable { /** * 數(shù)組初始容量大小 */ private static final int DEFAULT_INITIAL_CAPACITY = 11; /** * 數(shù)組,用于存儲(chǔ)元素 */ transient Object[] queue; // non-private to simplify nested class access /** * 元素個(gè)數(shù) */ private int size = 0; /** * 比較器,用于排序元素優(yōu)先級(jí) */ private final Comparator<? super E> comparator; }
可以看出PriorityQueue
底層是基于數(shù)組實(shí)現(xiàn)的,使用Object[]
數(shù)組存儲(chǔ)元素,并且定義了比較器comparator
,用于排序元素的優(yōu)先級(jí)。
初始化
PriorityQueue
常用的初始化方法有4個(gè):
- 無(wú)參構(gòu)造方法
- 指定容量大小的有參構(gòu)造方法
- 指定比較器的有參構(gòu)造方法
- 同時(shí)指定容量和比較器的有參構(gòu)造方法
/** * 無(wú)參構(gòu)造方法 */ PriorityQueue<Integer> blockingQueue1 = new PriorityQueue<>(); /** * 指定容量大小的構(gòu)造方法 */ PriorityQueue<Integer> blockingQueue2 = new PriorityQueue<>(10); /** * 指定比較器的有參構(gòu)造方法 */ PriorityQueue<Integer> blockingQueue3 = new PriorityQueue<>(Integer::compareTo); /** * 同時(shí)指定容量和比較器的有參構(gòu)造方法 */ PriorityQueue<Integer> blockingQueue4 = new PriorityQueue<>(10, Integer::compare);
再看一下對(duì)應(yīng)的源碼實(shí)現(xiàn):
/** * 無(wú)參構(gòu)造方法 */ public PriorityQueue() { // 使用默認(rèn)容量大小11,不指定比較器 this(DEFAULT_INITIAL_CAPACITY, null); } /** * 指定容量大小的構(gòu)造方法 */ public PriorityQueue(int initialCapacity) { this(initialCapacity, null); } /** * 指定比較器的有參構(gòu)造方法 */ public PriorityQueue(Comparator<? super E> comparator) { this(DEFAULT_INITIAL_CAPACITY, comparator); } /** * 同時(shí)指定容量和比較器的有參構(gòu)造方法 */ public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) { throw new IllegalArgumentException(); } this.queue = new Object[initialCapacity]; this.comparator = comparator; }
可以看出PriorityQueue
的無(wú)參構(gòu)造方法使用默認(rèn)的容量大小11,直接初始化數(shù)組,并且沒(méi)有指定比較器。
放數(shù)據(jù)源碼
放數(shù)據(jù)的方法有2個(gè):
操作 | 拋出異常 | 返回特定值 |
---|---|---|
放數(shù)據(jù) | add() | offer() |
offer方法源碼
先看一下offer()方法源碼,其他放數(shù)據(jù)方法邏輯也是大同小異,都是在鏈表尾部插入。 offer()方法在隊(duì)列滿的時(shí)候,會(huì)直接返回false,表示插入失敗。
/** * offer方法入口 * * @param e 元素 * @return 是否插入成功 */ public boolean offer(E e) { // 1. 判空,傳參不允許為null if (e == null) { throw new NullPointerException(); } modCount++; int i = size; // 2. 當(dāng)數(shù)組滿的時(shí)候,執(zhí)行擴(kuò)容 if (i >= queue.length) { grow(i + 1); } size = i + 1; // 3. 如果是第一次插入,就直接把元素插入到數(shù)組頭部 if (i == 0) { queue[0] = e; } else { // 4. 如果不是第一次插入,就找個(gè)合適的位置插入(需要保證插入后數(shù)組有序) siftUp(i, e); } return true; }
offer()方法邏輯也很簡(jiǎn)單,先判斷是否需要擴(kuò)容,如果需要擴(kuò)容先執(zhí)行擴(kuò)容邏輯,然后把元素插入到數(shù)組中。如果是第一次插入,就直接把元素插入到數(shù)組頭部。如果不是,就找個(gè)合適的位置插入,需要保證插入后數(shù)組仍是有序的。 再看一下擴(kuò)容的源碼:
/** * 擴(kuò)容 */ private void grow(int minCapacity) { int oldCapacity = queue.length; // 1. 如果原數(shù)組容量小于64,就執(zhí)行2倍擴(kuò)容,否則執(zhí)行1.5擴(kuò)容 int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // 2. 校驗(yàn)最大容量不能超過(guò)Integer最大值 if (newCapacity - MAX_ARRAY_SIZE > 0) { newCapacity = hugeCapacity(minCapacity); } // 3. 直接擴(kuò)容后新數(shù)組賦值給原數(shù)組 queue = Arrays.copyOf(queue, newCapacity); }
擴(kuò)容的源碼設(shè)計(jì)充滿了作者的巧思,在數(shù)組容量較小的時(shí)候,為了避免頻繁擴(kuò)容,就采用2倍擴(kuò)容法。在數(shù)組容量較大的時(shí)候,為了避免擴(kuò)容后浪費(fèi)空間,就采用1.5倍擴(kuò)容法。
PriorityQueue
為了快速的插入和刪除,采用了最小堆
,而不是直接使用有序數(shù)組,這樣既可以保證插入和刪除的時(shí)間復(fù)雜度都是O(logn),又能避免移動(dòng)過(guò)多元素。
最小堆的定義: 除葉子節(jié)點(diǎn)外,每個(gè)節(jié)點(diǎn)的值都小于等于左右子節(jié)點(diǎn)的值。
下面就是一個(gè)簡(jiǎn)單的最小堆和映射數(shù)組:
再看一下siftUp()方法源碼,是怎么保證插入元素,數(shù)組仍是有序的? 其實(shí)就是循環(huán)跟父節(jié)點(diǎn)比較元素大小,找個(gè)合適的位置插入。
// 把元素插入到合適的位置 private void siftUp(int k, E x) { // 1. 如果初始化的時(shí)候,自定義了比較器,就使用自定義比較器的插入方法,否則使用默認(rèn)的。 if (comparator != null) { siftUpUsingComparator(k, x); } else { siftUpComparable(k, x); } } // 自定義比較器的插入方法 private void siftUpUsingComparator(int k, E x) { while (k > 0) { // 1. 找到父節(jié)點(diǎn) int parent = (k - 1) >>> 1; Object e = queue[parent]; // 2. 如果當(dāng)前節(jié)點(diǎn)元素比父節(jié)點(diǎn)的元素小,就把父節(jié)點(diǎn)元素向下移動(dòng)(給當(dāng)前元素騰出位置) if (comparator.compare(x, (E) e) >= 0) { break; } queue[k] = e; k = parent; } // 3. 把當(dāng)前元素插入到父節(jié)點(diǎn)的位置 queue[k] = x; } // 默認(rèn)的插入方法 private void siftUpComparable(int k, E x) { // 1. 使用默認(rèn)比較器 Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { // 2. 找到父節(jié)點(diǎn) int parent = (k - 1) >>> 1; Object e = queue[parent]; // 3. 如果當(dāng)前節(jié)點(diǎn)元素比父節(jié)點(diǎn)的元素小,就把父節(jié)點(diǎn)元素向下移動(dòng)(給當(dāng)前元素騰出位置) if (key.compareTo((E) e) >= 0) { break; } queue[k] = e; k = parent; } // 4. 把當(dāng)前元素插入到父節(jié)點(diǎn)的位置 queue[k] = key; }
再看一下add()方法源碼:
add方法源碼
add()方法底層直接調(diào)用的是offer()方法,作用相同。
/** * add方法入口 * * @param e 元素 * @return 是否添加成功 */ public boolean add(E e) { return offer(e); }
彈出數(shù)據(jù)源碼
彈出數(shù)據(jù)(取出數(shù)據(jù)并刪除)的方法有2個(gè):
操作 | 拋出異常 | 返回特定值 |
---|---|---|
取數(shù)據(jù)(同時(shí)刪除數(shù)據(jù)) | remove() | poll() |
poll方法源碼
看一下poll()方法源碼,其他方取數(shù)據(jù)法邏輯大同小異,都是從數(shù)組頭部彈出元素。 poll()方法在彈出元素的時(shí)候,如果隊(duì)列為空,直接返回null,表示彈出失敗。
/** * poll方法入口 */ public E poll() { // 1. 如果數(shù)組為空,返回null if (size == 0) { return null; } int s = --size; modCount++; // 2. 暫存數(shù)組頭節(jié)點(diǎn),最后返回 E result = (E) queue[0]; // 3. 暫存數(shù)組尾節(jié)點(diǎn),調(diào)整最小堆的時(shí)候,需要上移 E x = (E) queue[s]; // 4. 刪除尾節(jié)點(diǎn) queue[s] = null; // 5. 調(diào)整最小堆 if (s != 0) { siftDown(0, x); } return result; }
remove方法源碼
再看一下remove()方法源碼,如果隊(duì)列為空,remove()會(huì)拋出異常。
/** * remove方法入口 */ public E remove() { // 1. 直接調(diào)用poll方法 E x = poll(); // 2. 如果取到數(shù)據(jù),直接返回,否則拋出異常 if (x != null) { return x; } else { throw new NoSuchElementException(); } }
查看數(shù)據(jù)源碼
再看一下查看數(shù)據(jù)源碼,查看數(shù)據(jù),并不刪除數(shù)據(jù)。
操作 | 拋出異常 | 返回特定值 |
---|---|---|
查看數(shù)據(jù)(不刪除) | element() | peek() |
peek方法源碼
先看一下peek()方法源碼,如果數(shù)組為空,直接返回null。
/** * peek方法入口 */ public E peek() { // 返回?cái)?shù)組頭節(jié)點(diǎn) return (size == 0) ? null : (E) queue[0]; }
element方法源碼
再看一下element()方法源碼,如果隊(duì)列為空,則拋出異常。
/** * element方法入口 */ public E element() { // 1. 調(diào)用peek方法查詢數(shù)據(jù) E x = peek(); // 2. 如果查到數(shù)據(jù),直接返回 if (x != null) { return x; } else { // 3. 如果沒(méi)找到,則拋出異常 throw new NoSuchElementException(); } }
總結(jié)
這篇文章講解了PriorityQueue
阻塞隊(duì)列的核心源碼,了解到PriorityQueue
隊(duì)列具有以下特點(diǎn):
PriorityQueue
實(shí)現(xiàn)了Queue
接口,提供了兩組放數(shù)據(jù)和讀數(shù)據(jù)的方法,來(lái)滿足不同的場(chǎng)景。PriorityQueue
底層基于數(shù)組實(shí)現(xiàn),按照最小堆存儲(chǔ),實(shí)現(xiàn)了高效的插入和刪除。PriorityQueue
初始化的時(shí)候,可以指定數(shù)組長(zhǎng)度和自定義比較器。PriorityQueue
初始容量是11,當(dāng)數(shù)組容量小于64,采用2倍擴(kuò)容,否則采用1.5擴(kuò)容。PriorityQueue
每次都是從數(shù)組頭節(jié)點(diǎn)取元素,取之后需要調(diào)整最小堆。
今天一起分析了PriorityQueue
隊(duì)列的源碼,可以看到PriorityQueue
的源碼非常簡(jiǎn)單,沒(méi)有什么神秘復(fù)雜的東西,下篇文章再一起接著分析其他的阻塞隊(duì)列源碼。
以上就是詳解Java中PriorityQueue的作用和源碼實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Java PriorityQueue的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot中的JdbcTemplate是什么及用法小結(jié)
Spring Boot中的JdbcTemplate是一個(gè)強(qiáng)大的數(shù)據(jù)庫(kù)訪問(wèn)工具,它簡(jiǎn)化了數(shù)據(jù)庫(kù)操作的過(guò)程,在本文中,我們了解了JdbcTemplate的基本概念,并演示了如何在Spring Boot應(yīng)用程序中使用它,感興趣的朋友跟隨小編一起看看吧2023-10-10java中的Timer和Timertask的關(guān)系解讀
本文詳細(xì)介紹了Java中的Timer和TimerTask類,包括它們之間的關(guān)系、API的使用方法、注意事項(xiàng)以及操作案例,Timer是一個(gè)調(diào)度器,而TimerTask是具體的任務(wù)類,Timer僅對(duì)應(yīng)一個(gè)線程,不保證任務(wù)執(zhí)行的精確性,但線程安全,一個(gè)Timer可以調(diào)度多個(gè)TimerTask2024-12-12Spring Security permitAll()不允許匿名訪問(wèn)的操作
這篇文章主要介紹了Spring Security permitAll()不允許匿名訪問(wèn)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06MyBatis學(xué)習(xí)筆記(二)之關(guān)聯(lián)關(guān)系
這篇文章主要介紹了MyBatis學(xué)習(xí)筆記(二)之關(guān)聯(lián)關(guān)系 的相關(guān)資料,需要的朋友可以參考下2016-02-02JDK1.7中HashMap的死循環(huán)問(wèn)題及解決方案
這篇文章主要為大家介紹了JDK1.7中HashMap的死循環(huán)問(wèn)題及解決方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10java實(shí)現(xiàn)PPT轉(zhuǎn)化為PDF
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)PPT轉(zhuǎn)化為PDF的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06SpringBoot集成Mybatis+xml格式的sql配置文件操作
這篇文章主要介紹了SpringBoot集成Mybatis+xml格式的sql配置文件操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Java編程實(shí)現(xiàn)鄰接矩陣表示稠密圖代碼示例
這篇文章主要介紹了Java編程實(shí)現(xiàn)鄰接矩陣表示稠密圖代碼示例,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11基于ClasspathResource路徑問(wèn)題的解決
這篇文章主要介紹了ClasspathResource路徑問(wèn)題的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08