Java 基礎(六)集合源碼解析 Queue

Queue

Queue繼承自 Collection,咱們先來看看類結構吧,代碼量比較少,我直接貼代碼了前端

public interface Queue<E> extends Collection<E> {
    boolean add(E var1);
    boolean offer(E var1);
    E remove();
    E poll();
    E element();
    E peek();
}複製代碼

從方法名上不太好猜每一個方法的做用,咱們直接來看 API 吧算法

~ 拋出異常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
檢查 element() peek()

好像就除了對增刪查操做增長了一個不拋出異常的方法,沒什麼特色吧,咱們繼續看描述~後端

在處理元素前用於保存元素的 collection。除了基本的 Collection 操做外,隊列還提供其餘的插入、提取和檢查操做。每一個方法都存在兩種形式:一種拋出異常(操做失敗時),另外一種返回一個特殊值(null 或 false,具體取決於操做)。插入操做的後一種形式是用於專門爲有容量限制的 Queue 實現設計的;在大多數實現中,插入操做不會失敗。api

就描述了這三組方法的區別,那麼之後我操做隊列儘可能用不拋出異常的方法總行了吧。另外也沒看出什麼名堂,那麼隊列這個接口究竟是規範了什麼行爲?我記得隊列好像是一種數據經常使用的結構,咱們來看看百度百科的定義吧數組

隊列是一種特殊的線性表,特殊之處在於它只容許在表的前端(front)進行刪除操做,而在表的後端(rear)進行插入操做,和棧同樣,隊列是一種操做受限制的線性表。進行插入操做的端稱爲隊尾,進行刪除操做的端稱爲隊頭。安全

看了百度百科的描述,才知道隊列規範了集合只容許在表前端刪除,在表後端插入。這不就是 FIFO 嘛~~bash

什麼是 FIFO?

FIFO 是英語 first in first out 的縮寫。先進先出,想象一下,在車輛在經過不容許超車的隧道時,是否是先進入隧道的車輛最早出隧道。數據結構

FIFO 有什麼用?

這個問題我回答不了,隊列只是一種數據結構,在某些特定的場合,用隊列實現效率會比較高。併發

Queue 的抽象實現類

AbstractQueue 是Queue 的抽象實現類,和Lst、Set 的抽象實現類同樣,AbstractQueue 也繼承自 AbstractCollection。
AbstractQueue 實現的方法很少,主要就 add、remove、element 三個方法的操做失敗拋出了異常。ide

Queue 的實現類

PriorityQueue 直接繼承自 AbstractQueue,而且除序列號接口外,沒實現任何接口,大概算是最忠誠的 Queue 實現類吧。照慣例,咱們先來看看 API 介紹。

一個基於優先級堆的無界優先級隊列。優先級隊列的元素按照其天然順序進行排序,或者根據構造隊列時提供的 Comparator 進行排序,具體取決於所使用的構造方法。優先級隊列不容許使用 null 元素。依靠天然順序的優先級隊列還不容許插入不可比較的對象.
此隊列的頭 是按指定排序方式肯定的最小 元素。若是多個元素都是最小值,則頭是其中一個元素——選擇方法是任意的。隊列獲取操做 poll、remove、peek 和 element 訪問處於隊列頭的元素。
優先級隊列是無界的,可是有一個內部容量,控制着用於存儲隊列元素的數組大小。它一般至少等於隊列的大小。隨着不斷向優先級隊列添加元素,其容量會自動增長。無需指定容量增長策略的細節。

進隊列的數據還要進行排序,每次取都是取到元素最小值,尼瑪,說好的 FIFO 呢?好吧,我暫且當這是一個取出時有順序的隊列,看起來和昨天學的 TreeSet 功能差很少哈。

PriorityQueue 叫優先隊列,即優先把元素最小值存到隊頭。想象一下,使用PriorityQueue去管理一個班的學生,根據能夠年齡、成績、身高設置好對應的 Comparator ,而後就能自動從小到大排序呢。哈哈哈~

咱們先來看一下 PriorityQueue 的實現吧~

類成員變量以下~

public class PriorityQueue<E> extends AbstractQueue<E> implements Serializable {
    private static final long serialVersionUID = -7720805057305804111L;
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    transient Object[] queue;
    private int size;
    private final Comparator<? super E> comparator;
    transient int modCount;
    private static final int MAX_ARRAY_SIZE = 2147483639;
}複製代碼

沒錯,基於數組的實現,也能找到 grow 擴容方法,少了 List 的各類方法,Queue 的方法咱們前面也看了。那麼咱們就以前去看他是怎麼實現優先隊列的~

思考一下,既然是數組實現,又能按元素大小順序去取出,那麼確定是在添加元素的時候作的排序,直接把對應的元素值大小的元素添加到對應的位置。那麼咱們就從 add 方法看起吧~~

public boolean add(E var1) {
    return this.offer(var1);
}

public boolean offer(E var1) {
    if(var1 == null) {
        throw new NullPointerException();
    } else {
        ++this.modCount;
        int var2 = this.size;
        if(var2 >= this.queue.length) {
            this.grow(var2 + 1);
        }

        this.size = var2 + 1;
        if(var2 == 0) {
            this.queue[0] = var1;
        } else {
            this.siftUp(var2, var1);
        }

        return true;
    }
}
private void siftUp(int childIndex) {
    E target = elements[childIndex];
    int parentIndex;
    while (childIndex > 0) {
        parentIndex = (childIndex - 1) / 2;
        E parent = elements[parentIndex];
        if (compare(parent, target) <= 0) {
            break;
        }
        elements[childIndex] = parent;
        childIndex = parentIndex;
    }
    elements[childIndex] = target;
}複製代碼

上面的方法調用都很簡單,我就不寫註釋了,add 調用 offer 添加元素,若是集合裏面的元素個數不爲零,則調用 siftUp 方法把元素插入合適的位置。

敲黑板~~接下來的東西我看了老半天才看明白。有點吃力

注意了,siftUp裏面的算法有點奇怪,我一開始還覺得是二分插入法,然而並非。

首先,咱們這裏走進了一個誤區,PriorityQueue 雖然是一個優先隊列,可以知足咱們剛剛說的需求,把一個班的學生按年齡大小順序取出來,可是在內存中(數組中)的保存卻並非按照從小到大的順序保存的,可是一直 poll,是可以按照元素從小到大的順去取出結果。

這裏我作了一個小測試。

PriorityQueue<Integer> integers = new PriorityQueue<>();
integers.add(8);                                        
integers.add(6);                                       
integers.add(5);                                       複製代碼

已知 PriorityQueue 用數組存儲,你們猜猜我這樣存進隊列的三個數子是怎樣存儲的?
一開始我覺得是五、六、8的順序,可是 debug 的時候看到 PriorityQueue 裏面保存數據數組裏面的存放順序是五、八、6.why?

而後我調用下面這個方法打印~

while (!integers.isEmpty()) {              
    Log.e("_____", integers.poll() + "~~");
}                                          複製代碼

結果是五、六、8.這他媽就尷尬了。

而後怎麼辦~去找度娘唄。。。

好了,開始解析~~

不知道你們記不記得一種數據結構叫二叉樹,這裏就是使用了二叉樹的思路,因此比較難理解。

首先,這裏使用的是一種特殊的二叉樹:1.父節點永遠小於子節點,2.優先填滿第 n 層樹枝再填 n+1 層樹枝。也就是說,數組裏面的五、八、6是這樣存儲的

依次添加元素八、六、5.
  5                            
 / \    
8   6    
    ‖
    ∨
數組角標位置
  0
 / \
1   2複製代碼

這樣能理解了吧,再回過頭去看siftUp方法,捋一下添加元素的過程。

  • 添加8
    沒什麼好說的,直接添加一個元素到到數組[0]便可,二叉樹添加一個頂級節點

  • 添加5
    首先把[1]的位置賦值給5,使得數組中的元素爲{8,5}
    而後執行siftUp(1)方法(1是剛剛插入元素5的角標)

    siftUp方法首先獲取5的父節點,判斷5是否小於父節點。
      若是小於,則交換位置繼續比較祖父節點
      若是大於或者已經到頂級節點,結束。複製代碼

    siftUp方法後,數組變爲{5,8}

  • 添加6
    重複上面的動做,數組變爲{5,8,6}

問:若是此時添加數字7,數組的順序是多少?
思考一下3分鐘~~

好,3分鐘過去了,結果是{5,7,6,8}
爲何會這樣?拿着數字7代入到上面的方法中去算呀,首先8在數組中的角標是3,3要去和父節點比,求父節點的公式是(3-1)/2 = 1.因而父節點的角標是1,7<8,所以交換位置,此時角標1還有父節點 (1-1)/2 = 0,再比較7和5,7>5,知足大於父節點條件,結束。

好了,如今應該明白了吧~~~沒明白再回過頭去理解一遍。
接下來,咱們來看循環調用 poll() 方法是怎樣從{5,8,6}的數組中按照從小到大的順序取出五、六、8.
咱們來看 poll()方法

public E poll() {
    if (isEmpty()) {
        return null;
    }
    E result = elements[0];
    removeAt(0);
    return result;
}
private void removeAt(int index) {
    size--;
    E moved = elements[size];
    elements[index] = moved;
    siftDown(index);
    elements[size] = null;
    if (moved == elements[index]) {
        siftUp(index);
    }
}
private void siftDown(int rootIndex) {
    E target = elements[rootIndex];
    int childIndex;
    while ((childIndex = rootIndex * 2 + 1) < size) {
        if (childIndex + 1 < size
                    && compare(elements[childIndex + 1], elements[childIndex]) < 0) {
            childIndex++;
        }
        if (compare(target, elements[childIndex]) <= 0) {
            break;
        }
        elements[rootIndex] = elements[childIndex];
        rootIndex = childIndex;
    }
    elements[rootIndex] = target;
}複製代碼

這是 api23 裏面 PriorityQueue 的方法,和 Java8 略有不一樣,但實現都是同樣的,只是方法看起來好理解一些。

首先 poll 方法取出了數組角標0的值,這點不用質疑,由於角標0對應二叉樹的最高節點,也就是最小值。

而後在 removeAt 方法裏面把數組的最後一個元素覆蓋了第0個元素,再是將最後一個元素置空,好,到了這裏,進入第二個關鍵點了,黑板敲起來~~

這裏在賦值以後調用了 siftDown(0);
咱們來看 siftDown()方法~
這個方法從0角標(最頂級父節點)開始,先判斷左右子節點,取較小的那個一,和父節點比較,而後再對比左右子節點。根據咱們這裏二叉樹的特色,最終能取到最小的那個元素放到頂級父節點,保證下一次 poll能取到當前集合最小的元素。具體代碼不帶着讀了~~

ok,PriorityQueue 看完了。

#Deque
剛剛咱們一直在找 FIFO 的集合,找到個 PriorityQueue,然而並非。
而後咱們繼續找唄,發現了 Queue 有一個子接口Deque

來看看 API 文檔的定義~

一個線性 collection,支持在兩端插入和移除元素。名稱 deque 是「double ended queue(雙端隊列)」的縮寫,一般讀爲「deck」。大多數 Deque 實現對於它們可以包含的元素數沒有固定限制,但此接口既支持有容量限制的雙端隊列,也支持沒有固定大小限制的雙端隊列。

此接口定義在雙端隊列兩端訪問元素的方法。提供插入、移除和檢查元素的方法。每種方法都存在兩種形式:一種形式在操做失敗時拋出異常,另外一種形式返回一個特殊值(null 或 false,具體取決於操做)。插入操做的後一種形式是專爲使用有容量限制的 Deque 實現設計的;在大多數實現中,插入操做不能失敗。

嗯~就是一個首尾插入刪除操做都直接的接口。

咱們剛剛說了 Queue 遵循 FIFO 規則,當有了 Deque,咱們還能實現 LIFO(後進先出)。反正像先進後出、後進先出都能在 Deque 的實現類上作到,具體看各位 Coder 們怎麼操做了。

總結一下 Deque 的方法~

~~-- __第一個元素(頭部)..... _最後一個元素(尾部)
~ 拋出異常 特殊值 拋出異常 特殊值
插入 addFirst(e) offerFirst(3) addLast(e) offerLast(3)
移除 removeFirst() pollFirst() removeLast() pollLast()
檢查 getFirst() peekFirst() getLast() peekLast()

__特麼的,MD 語法不支持這種不對齊表格

若是想用做 LIFO 隊列,應優先使用此接口,而不是遺留的 Stack 類。在將雙端隊列用做堆棧時,元素被推入雙端隊列的開頭並從雙端隊列開頭彈出。堆棧方法徹底等效於 Deque 方法,以下表所示:

堆棧方法 等效 Deque 方法
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()

就醬紫吧,也沒什麼特別的,我我的不太喜歡這個接口,我以爲這個接口規範的行爲有點多,不符合接口隔離原則和單一職能原則。

接下來咱們就去看看 Deque 的實現類吧。

看兩個具備表明性的類吧,第一個是基於數組實現的 ArrayQeque,第二個是基於鏈表實現的LinkedList。

LinkedList

前面 List 的時候咱們看過 LinkedList,LinkedList 繼承自AbstractList,同時也實現了 List 接口,所以這是一個很全能的類。一句話描述就是:基於鏈表結構實現的數組,同時又支持雙向隊列操做。

還記得以前在 List 結尾留的一個思考題麼:怎樣用鏈表的結構快速實現棧功能LinkedListStack?

public class LinkedListStack extends LinkedList{
    public LinkedListStack(){
        super();
    }

    @Override
    public void push(Object o) {
        super.push(o);
    }

    @Override
    public Object pop() {
        return super.pop();
    }

    @Override
    public Object peek() {
        return super.peek();
    }

    @Override
    public boolean isEmpty() {
        return super.isEmpty();
    }

    public int search(Object o){
        return indexOf(o);
    }
}複製代碼

吶,這裏給出了實現,其實什麼都沒作,就是調用了父類方法。這個類只是看起來結構清晰的實現了 LIFO,可是因爲繼承自 LinkedList,仍是能夠調用 addFirst 等各類「非法操做方法」,這就是我說的不理解 Java 爲何要這樣設計,還推薦使用 Deque 替換棧實現。項目實際開發中,同窗們要使用棧結構直接用 LinkedList就好了,我這裏 LinkedListStack 只是便於你們理解 LinkedList 也能夠用做棧集合。

ArrayDeque

照慣例先看 API 定義~

Deque接口的大小可變數組的實現。數組雙端隊列沒有容量限制;它們可根據須要增長以支持使用。它們不是線程安全的;在沒有外部同步時,它們不支持多個線程的併發訪問。禁止 null 元素。此類極可能在用做堆棧時快於 Stack,在用做隊列時快於 LinkedList。

感受 ArrayDeque 纔是一個正常的 Deque 實現類,ArrayDeque 直接繼承自 AbstractCollection,實現了Deque接口。

類部實現和 ArrayList 同樣都是基於數組,當頭尾下標相等時,調用doubleCapacity()方法,執行翻倍擴容操做。

頭尾操做是什麼鬼?咱們都知道ArrayDeque 是雙向列表,就是能夠兩端一塊兒操做的列表。所以使用了兩個指針 head 和tail 來保存當前頭尾的 index,一開始默認都是0角標,當添加一個到尾的時候,tail先加1,再把值存放到 tail 角標的數組裏面去。
那麼 addFirst 是怎麼操做的呢?head 是0,添加到-1的角標上面去?其實不是的,這裏 你能夠把這個數組當成是一個首尾相連的鏈表,head 是0的時候 addFirst 其實是把值存到了數組最後一個角標裏面去了。即: 當 head 等於0的時候 head - 1 的值 數組.length - 1,代碼實現以下。

如圖,這是我以下代碼的執行添加60時 debug

ArrayDeque<Integer> integers = new ArrayDeque<>();
integers.addLast(8);
integers.addFirst(60);複製代碼

而後當head == tail的時候表示數組用滿了,須要擴容,就執行doubleCapacity擴容,這裏的擴容和 ArrayList 的代碼差很少,就不去分析了。

總結

凡是牽涉到須要使用 FIFO 或者 LIFO 的數據結構時,推薦使用 ArrayDeque,LinkedList 也行,還有 get(index)方法~~

相關文章
相關標籤/搜索