Java集合源碼分析之Queue(三):ArrayDeque_一點課堂(多岸學院)

在介紹了QueueDeque概念以後,這是要進行分析的第一個實現類。ArrayDeque可能你們用的都比較少,但其實現裏有許多亮點仍是值得咱們關注的。java

Deque的定義爲double ended queue,也就是容許在兩側進行插入和刪除等操做的隊列。這個定義看起來很簡單,那麼咱們怎麼實現它呢?咱們最容易想到的就是使用雙向鏈表。咱們在前文介紹過單鏈表,其每一個數據單元都包含一個數據元素和一個指向下一個元素位置的指針next,這樣的鏈表只能從前向後遍歷。若是咱們要把它變成雙向的,只須要添加一個能夠指向上一個元素位置的指針previous,同時記錄下其尾節點便可。LinkedList的實現就是採用了這一實現方案。數組

ArrayDeque又是什麼,它的結構又是怎樣的呢?咱們先看下其文檔吧:安全

Resizable-array implementation of the Deque interface. Array deques have no capacity restrictions; they grow as necessary to support usage. They are not thread-safe; in the absence of external synchronization, they do not support concurrent access by multiple threads. Null elements are prohibited. This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.多線程

文檔中並無過多的介紹實現細節,但說它是Resizable-array implementation of the Deque interface,也就是用可動態調整大小的數組來實現了Deque,聽起來是否是像ArrayList?但ArrayDeque對數組的操做方式和ArrayList有較大的差異。下面咱們就深刻其源碼看看它是如何巧妙的使用數組的,以及爲什麼說函數

faster than Stack when used as a stack, and faster than LinkedList when used as a queue.學習

構造函數與重要成員變量

ArrayDeque共有四個成員變量,其中兩個咱們在分析ArrayList時已經見過了,還有兩個咱們須要認真研究一下:this

//存放元素,長度和capacity一致,而且老是2的次冪
//這一點,咱們放在後面解釋
transient Object[] elements; 

//capacity最小值,也是2的次冪
private static final int MIN_INITIAL_CAPACITY = 8;

//標記隊首元素所在的位置
transient int head;

//標記隊尾元素所在的位置
transient int tail;

其構造函數共有三個:線程

//默認構造函數,將elements長度設爲16,至關於最小capacity的兩倍
public ArrayDeque() {
    elements = new Object[16];
}

//帶初始大小的構造
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

//從其餘集合類導入初始數據
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

這裏看到有兩個構造函數都用到了allocateElements方法,這是一個很是經典的方法,咱們接下來就先重點研究它。指針

尋找最近的2次冪

在定義elements變量時說,其長度老是2的次冪,但用戶傳入的參數並不必定符合規則,因此就須要根據用戶的輸入,找到比它大的最近的2次冪。好比用戶輸入13,就把它調整爲16,輸入31,就調整爲32,等等。考慮下,咱們有什麼方法能夠實現呢?rest

來看下ArrayDeque是怎麼作的吧:

private void allocateElements(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    elements = new Object[initialCapacity];
}

看到這段迷之代碼了嗎?在HashMap中也有一段相似的實現。但要讀懂它,咱們須要先掌握如下幾個概念:

  • 在java中,int的長度是32位,有符號int能夠表示的值範圍是 (-2)^31^ 到 2^31^-1,其中最高位是符號位,0表示正數,1表示負數。
  • >>>:無符號右移,忽略符號位,空位都以0補齊。
  • |:位或運算,按位進行或操做,逢1爲1。

咱們知道,計算機存儲任何數據都是採用二進制形式,因此一個int值爲80的數在內存中多是這樣的:

0000 0000 0000 0000 0000 0000 0101 0000

比80大的最近的2次冪是128,其值是這樣的:

0000 0000 0000 0000 0000 0000 1000 0000

咱們多找幾組數據就能夠發現規律:

  • 每一個2的次冪用二進制表示時,只有一位爲 1,其他位均爲 0(不包含符合位)
  • 要找到比一個數大的2的次冪(在正數範圍內),只須要將其最高位左移一位(從左往右第一個 1 出現的位置),其他位置 0 便可。

但從實踐上講,沒有可行的方法可以進行以上操做,即便經過&操做符能夠將某一位置 0 或置 1,也沒法確認最高位出現的位置,也就是基於最高位進行操做不可行。

但還有一個很整齊的數字能夠被咱們利用,那就是 2^n^-1,咱們看下128-1=127的表示形式:

0000 0000 0000 0000 0000 0000 0111 1111

把它和80對比一下:

0000 0000 0000 0000 0000 0000 0101 0000 //80 0000 0000 0000 0000 0000 0000 0111 1111 //127

能夠發現,咱們只要把80從最高位起每一位全置爲1,就能夠獲得離它最近且比它大的 2^n^-1,最後再執行一次+1操做便可。具體操做步驟爲(爲了演示,這裏使用了很大的數字): 原值:

0011 0000 0000 0000 0000 0000 0000 0010

  1. 無符號右移1位

0001 1000 0000 0000 0000 0000 0000 0001

  1. 與原值|操做:

0011 1000 0000 0000 0000 0000 0000 0011

能夠看到最高2位都是1了,也僅能保證前兩位爲1,這時就能夠直接移動兩位

  1. 無符號右移2位

0000 1110 0000 0000 0000 0000 0000 0000

  1. 與原值|操做:

0011 1110 0000 0000 0000 0000 0000 0011

此時就能夠保證前4位爲1了,下一步移動4位

  1. 無符號右移4位

0000 0011 1110 0000 0000 0000 0000 0000

  1. 與原值|操做:

0011 1111 1110 0000 0000 0000 0000 0011

此時就能夠保證前8位爲1了,下一步移動8位

  1. 無符號右移8位

0000 0000 0011 1111 1110 0000 0000 0000

  1. 與原值|操做:

0011 1111 1111 1111 1110 0000 0000 0011

此時前16位都是1,只須要再移位操做一次,便可把32位都置爲1了。

  1. 無符號右移16位

0000 0000 0000 0000 0011 1111 1111 1111

  1. 與原值|操做:

0011 1111 1111 1111 1111 1111 1111 1111

  1. 進行+1操做:

0100 0000 0000 0000 0000 0000 0000 0000

如此通過11步操做後,咱們終於找到了合適的2次冪。寫成代碼就是:

initialCapacity |= (initialCapacity >>>  1);
    initialCapacity |= (initialCapacity >>>  2);
    initialCapacity |= (initialCapacity >>>  4);
    initialCapacity |= (initialCapacity >>>  8);
    initialCapacity |= (initialCapacity >>> 16);
    initialCapacity++;

不過爲了防止溢出,致使出現負值(若是把符號位置爲1,就爲負值了)還須要一次校驗:

if (initialCapacity < 0)   // Too many elements, must back off
     initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements

至此,初始化的過程就完畢了。

重要操做方法

add分析

Deque主要定義了一些關於First和Last的操做,如add、remove、get等。咱們看看它是如何實現的吧。

//在隊首添加一個元素,非空
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

//在隊尾添加一個元素,非空
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

這裏,又有一段迷之代碼須要咱們認真研究了,這也是ArrayDeque值得咱們研究的地方之一,經過位運算提高效率。

elements[head = (head - 1) & (elements.length - 1)] = e;

很明顯這是一個賦值操做,並且應該是給head以前的位置賦值,因此head = (head - 1)是合理的操做,那這個& (elements.length - 1)又表示什麼呢?

在以前的定義與初始化中,elements.length要求爲2的次冪,也就是 2^n^ 形式,那這個& (elements.length - 1)也就是 2^n^-1 了,在內存中用二進制表示就是從最高位起每一位都是1。咱們還以以前的127爲例:

0000 0000 0000 0000 0000 0000 0111 1111

&就是按位與,全1才爲1。那麼任意一個正數和127進行按位與操做後,都只有最右側7位被保留了下來,其餘位所有置0(除符號位),而對一個負數而言,則會把它的符號位置爲0,&操做後會變成正數。好比-1的值是1111 ... 1111(32個1),和127按位操做後結果就變成了127 。因此,對於正數它就是取模,對於負數,它就是把元素插入了數組的結尾。因此,這個數組並非向前添加元素就向前擴展,向後添加就向後擴展,它是循環的,相似這樣:

file

初始時,head與tail都指向a[0],這時候數組是空的。當執行addFirst()方法時,head指針移動一位,指向a[elements.length-1],並賦值,也就是給a[elements.length-1]賦值。當執行addLast()操做時,先給a[0]賦值,再將tail指針移動一位,指向a[1]。因此執行完以後head指針位置是有值的,而tail位置是沒有值的。

隨着添加操做執行,數組總會佔滿,那麼怎麼判斷它滿了而後擴容呢?首先,若是head==tail,則說明數組是空的,因此在添加元素時必須保證head與tail不相等。假如如今只有一個位置能夠添加元素了,相似下圖:

file

此時,tail指向了a[8],head已經填充到a[9]了,只有a[8]是空閒的。很顯然,不論是addFirst仍是addLast,再添加一個元素後都會致使head==tail。這時候就不得不擴容了,由於head==tail是判斷是否爲空的條件。擴容就比較簡單了,直接翻倍,咱們看代碼:

private void doubleCapacity() {
    //只有head==tail時才能夠擴容
    assert head == tail;
    int p = head;
    int n = elements.length;
    //在head以後,還有多少元素
    int r = n - p; // number of elements to the right of p
    //直接翻倍,由於capacity初始化時就已是2的倍數了,這裏無需再考慮
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    //左側數據拷貝
    System.arraycopy(elements, p, a, 0, r);
    //右側數據拷貝
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

分析完add,那麼get以及remove等都大同小異,感興趣能夠查看源碼。咱們還要看看在Deque中定義的removeFirstOccurrenceremoveLastOccurrence方法的具體實現。

Occurrence相關

removeFirstOccurrenceremoveLastOccurrence分別用於找到元素在隊首或隊尾第一次出現的位置並刪除。其實現原理是一致的,咱們分析一個便可:

public boolean removeFirstOccurrence(Object o) {
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    Object x;
    while ( (x = elements[i]) != null) {
        if (o.equals(x)) {
            delete(i);
            return true;
        }
        i = (i + 1) & mask;
    }
    return false;
}

這裏就是遍歷全部元素,而後經過delete方法刪除,咱們看看delete實現:

private boolean delete(int i) {
    //檢查
    checkInvariants();
    final Object[] elements = this.elements;
    final int mask = elements.length - 1;
    final int h = head;
    final int t = tail;
    //待刪除元素前面的元素個數
    final int front = (i - h) & mask;
    //待刪除元素後面的元素個數
    final int back  = (t - i) & mask;

    // Invariant: head <= i < tail mod circularity
    //確認 i 在head和tail之間
    if (front >= ((t - h) & mask))
        throw new ConcurrentModificationException();

    // Optimize for least element motion
    //儘可能最少操做數據
    //前面數據比較少
    if (front < back) {
        if (h <= i) {
            //這時 h 和 i 之間最近距離沒有跨過位置0
            System.arraycopy(elements, h, elements, h + 1, front);
        } else { // Wrap around
            System.arraycopy(elements, 0, elements, 1, i);
            elements[0] = elements[mask];
            System.arraycopy(elements, h, elements, h + 1, mask - h);
        }
        elements[h] = null;
        head = (h + 1) & mask;
        return false;
    } else {
        if (i < t) { // Copy the null tail as well
         //這時 t 和 i 之間最近距離沒有跨過位置0
            System.arraycopy(elements, i + 1, elements, i, back);
             tail = t - 1;
        } else { // Wrap around
            System.arraycopy(elements, i + 1, elements, i, mask - i);
            elements[mask] = elements[0];
            System.arraycopy(elements, 1, elements, 0, t);
            tail = (t - 1) & mask;
        }
        return true;
    }
}

總結

ArrayDeque經過循環數組的方式實現的循環隊列,並經過位運算來提升效率,容量大小始終是2的次冪。當數據充滿數組時,它的容量將翻倍。做爲Stack,由於其非線程安全因此效率高於java.util.Stack,而做爲隊列,由於其不須要結點支持因此更快(LinkedList使用Node存儲數據,這個對象頻繁的new與clean,使得其效率略低於ArrayDeque)。但隊列更多的用來處理多線程問題,因此咱們更多的使用BlockingQueue,關於多線程的問題,之後再認真研究。


【感謝您能看完,若是可以幫到您,麻煩點個贊~】

更多經驗技術歡迎前來共同窗習交流: 一點課堂-爲夢想而奮鬥的在線學習平臺 http://www.yidiankt.com/

![關注公衆號,回覆「1」免費領取-【java核心知識點】] file

QQ討論羣:616683098

QQ:3184402434

想要深刻學習的同窗們能夠加我QQ一塊兒學習討論~還有全套資源分享,經驗探討,等你哦! 在這裏插入圖片描述

相關文章
相關標籤/搜索