1. 程式人生 > 其它 >Queue之ArrayDeque原始碼解析

Queue之ArrayDeque原始碼解析

目錄

1 Queue

1.1 定義

前面講了Stack是一種先進後出的資料結構:,那麼對應的Queue是一種先進先出(First In First Out)的資料結構:佇列
對比一下StackQueue是一種先進先出的容器,它有兩個口,從一個口放入元素,從另一個口獲取元素。如果把棧比作一個木桶,那麼佇列就是一個管道。

佇列有兩個口,一個負責入隊另一個負責出隊,所以會有先進先出的效果。
當然我們說ArrayDeque是一個雙向佇列,佇列的兩個口都可以入隊和出隊操作。再進一步說,其實ArrayDeque

可以說成是一個雙向迴圈佇列,是不是和連結串列的分類很像。

 public class ArrayDeque<E> extends AbstractCollection<E>
                            implements Deque<E>, Cloneable, Serializable

ArrayDeque的定義可以看到,它繼承AbstractCollection,實現了Deque,Cloneable,Serializable介面。Deque介面在LinkedList中見過,LinkedList也是實現Deque介面的。Deque

是一個雙端佇列,它實現於Queue介面,那麼什麼是雙端佇列,就是在佇列的同一端即可以入隊又可以出隊,所以Deque既可以作為佇列又可以作為棧使用。但是今天這裡是講Queue佇列,所以就只看單向佇列的一些原理和實現。

來看下Queue介面:

  public interface Queue<E> extends Collection<E> {
      // 增加一個元素到隊尾,如果佇列已滿,則丟擲一個IIIegaISlabEepeplian異常 
           boolean add(E e);
      // 新增一個元素到隊尾並返回true,如果佇列已滿,則返回false 
           boolean offer(E e);
      // 移除並返回佇列頭部的元素,如果佇列為空,則丟擲一個NoSuchElementException異常 
           E remove();
      // 移除並返問佇列頭部的元素,如果佇列為空,則返回null 
           E poll();
     // 返回佇列頭部的元素,如果佇列為空,則丟擲一個NoSuchElementException異常
          E element();
     // 返問佇列頭部的元素,如果佇列為空,則返回null
          E peek();
}

看到Queue的定義,發現它和Stack的方法是非常相似的。
但是ArrayDeque並不是一個固定大小的佇列,每次佇列滿了就會進行擴容,除非擴容至超過int的邊界,才會丟擲異常。所以這裡的addoffer幾乎是沒有區別的。

1.2 底層儲存

當然從ArrayDeque的命名就可以看出底層是用陣列實現的(而LinkedList則是用連結串列實現的佇列),來主要看一下ArrayDeque

     // 底層用陣列儲存元素
         private transient E[] elements;
      // 佇列的頭部元素索引(即將pop出的一個)
             private transient int head;
      // 佇列下一個要新增的元素索引
             private transient int tail;
      // 最小的初始化容量大小,需要為2的n次冪
      private static final int MIN_INITIAL_CAPACITY = 8;

這裡需要注意的是MIN_INITIAL_CAPACITY,這個初始化容量必須為2的n次冪。為什麼必須要是2的n次冪呢,HashMap要求其底層陣列的初始容量必須為2的n次冪。那麼ArrayDeque這裡又是基於什麼考慮呢,我們下面再看。
tail不是最後一個元素的索引,是下一個要新增的元素索引,也就是最後一個元素+1。

1.3 構造方法

      /** 
            * 預設構造方法,陣列的初始容量為16
       */ 
            public ArrayDeque() {
          elements = (E[]) new Object[16];
      }
   
      /**       * 使用一個指定的初始容量構造一個ArrayDeque
      */
public ArrayDeque( int numElements) {
         allocateElements(numElements);
}
 
     /**      * 構造一個指定Collection集合引數的ArrayDeque
      */
           public ArrayDeque(Collection<? extends E> c) {
         allocateElements(c.size());
         addAll(c);
     }
 
     /**
           * 分配合適容量大小的陣列,確保初始容量是大於指定numElements的最小的2的n次冪
      */
           private void allocateElements(int numElements) {
         int initialCapacity = MIN_INITIAL_CAPACITY;
         // 找到大於指定容量的最小的2的n次冪
         // Find the best power of two to hold elements.
         // Tests "<=" because arrays aren't kept full.
         // 如果指定的容量小於初始容量8,則執行一下if中的邏輯操作
         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 = (E[]) new Object[initialCapacity];
}

我們來仔細分析一下,回想一下在HashMap中分析過的,2的n次冪2的n次冪-1的二進位制是什麼樣子的呢,再來看一下:

2^n轉換為二進位制是什麼樣子呢:

2^1 = 10
2^2 = 100
2^3 = 1000
2^n = 1(n個0)

再來看下2^n-1的二進位制是什麼樣子的:

2^1 - 1 = 01
2^2 - 1 = 011
2^3 - 1 = 0111
2^n - 1 = 0(n個1)

看下程式碼initialCapacity++是什麼意思呢,就是說initialCapity+1之後才是2的n次冪,那麼此時的initialCapacity是什麼呢?就是上面的2^n - 1(initialCapacity + 1 = 2^n),也就是說怎麼做到使initialCapacity2^n - 1呢,那就上面的4次>>>|操作了。
>>>是無符號右移,意思就是將一個運算元轉換為二進位制後,將後n位移除,高位補0。舉個例子:11的二進位制1101,11 >>> 2就是:(1)將後兩位01移除,(2)高位補0,最後得0011。
|是按位或操作,意思是把兩個運算元分別轉換為二進位制,如果兩個運算元的位都有1則為1,全為0則為0,舉個例子:兩個數8和9的二進位制分別為1000和1001,1000 | 1001 = 1001。

理解了>>>|操作後,再來看下上面程式碼中的4個>>>|是什麼意思,>>>將一個數低位變為1,|後,最後整個數的二進位制都變為1。
舉個例子:如果initialCapacity=9,9轉換為二進位制為:1001,那麼經過第一輪>>>1後為:100,然後1001 | 100 = 1101;經過第二輪>>>2後變為:0011,然後1101 | 0011 = 1111,1111轉換為10進位制+1後等於16(2^4),到此經過這一系列的操作就完成獲取大於指定容量最小的2的n次冪。如果給定的initialCapacity夠大的話,最終將變為1111111111111111111111111111111(31位1),當然最後為了防止溢位(initialCapacity<0),將initialCapacity右移1位變成2的30次方,那麼什麼時候initialCapacity會小於0呢,那就是當initialCapacity作為int值<<1越界後。

其實在HashMap中也有這麼一個目的的操作,只不過其程式碼不是這麼實現的,它是通過一個迴圈,每次迴圈只右移1位。來回憶一下:

     // 確保容量為2的n次冪,是capacity為大於initialCapacity的最小的2的n次冪
      int capacity = 1;
      while (capacity < initialCapacity)
          capacity <<= 1;

那麼這兩種方法有什麼區別呢?HashMap中的這種寫法更容量理解,而ArrayDeque中的效果更高(最多經過4次位移和或操作+1次加一操作)。

1.4 入隊(新增元素到隊尾)

      /**       * 增加一個元素,如果佇列已滿,則丟擲一個IIIegaISlabEepeplian異常
       */ 
            public boolean add(E e) {
          // 呼叫addLast方法,將元素新增到隊尾 
                   addLast(e);
          return true;
      }
  
      /**      * 新增一個元素
      */
           public boolean offer(E e) {
         // 呼叫offerLast方法,將元素新增到隊尾
                  return offerLast(e);
     }
 
     /**
           * 在隊尾新增一個元素
      */     public boolean offerLast(E e) {
         // 呼叫addLast方法,將元素新增到隊尾
                  addLast(e);
         return true;
     }
 
     /**
           * 將元素新增到隊尾
      */
           public void addLast(E e) {
         // 如果元素為null,咋丟擲空指標異常
                  if (e == null)
             throw new NullPointerException();
         // 將元素e放到陣列的tail位置
                  elements[tail ] = e;
         // 判斷tail和head是否相等,如果相等則對陣列進行擴容
                  if ( (tail = (tail + 1) & ( elements.length - 1)) == head)
             // 進行兩倍擴容
                          doubleCapacity();
     }

這裡,( (tail = (tail + 1) & ( elements.length - 1)) == head)這句程式碼是關鍵,為什麼會這樣寫呢。正常的新增元素後應該是將tail+1對不對,但是佇列的刪除和新增是不在同一端的,什麼意思呢,我們畫個圖看一下。

我們假設佇列的初始容量是8,初始佇列添加了4個元素A、B、C、D,分別在陣列0、1、2、3的下標位置,如左圖,此時的head對應陣列下標0,tail對應陣列下標4。當佇列經過一系列的入隊和出隊後,就會變成右圖的樣子,此時的head對應陣列下標3,tail對應陣列下標7 。那麼問題來了,如果這個時候再增加一個元素到陣列下標7的位置,此時理論上tail+1=8,也就是已經越界,需要對陣列進行擴容了,但是我們看下陣列0、1、2的位置由於出隊操作,這三個位置是空的,如果此時就進行擴容會造成空間的浪費。

我們回想一下ArrayList為了減少空間浪費,它是怎麼做的呢,是通過陣列copy,每次刪除元素都會將被刪除元素索引後面位置的元素向前移動一位。但是這樣做又造成了效率不高。怎麼辦呢,能不能換一種思路,我們可以把陣列想象成為一個首尾相連的,陣列的第一個位置索引0的位置和陣列的最後一個位置索引length-1的位置是挨在一起的(雙向連結串列)。
需要注意的是head不是陣列的第一個位置索引0tail也不是陣列的最後一個位置索引length-1headtail實際上是一個指標,隨著出隊和入隊操作不斷的移動。如果tail移動到length-1之後,如果陣列的第一個位置0沒有元素,那麼需要將tail指向0,依次向後指向。此時當tail如果等於head的時候會有兩種情況,一個是空佇列,另一個就是佇列將要滿了(只有tail處還有空位置),只要判斷佇列將要滿了的時候,就進行陣列擴容。
再來回憶下2的n次冪2的n次冪-1轉換成二進位制後的樣子:

2^n轉換為二進位制是什麼樣子呢:

2^1 = 10
2^2 = 100
2^3 = 1000
2^n = 1(n個0)

再來看下2^n-1的二進位制是什麼樣子的:

2^1 - 1 = 01
2^2 - 1 = 011
2^3 - 1 = 0111
2^n - 1 = 0(n個1)

會發現什麼,如果(2^n) & (2^n-1) = 0對不對,舉個例子,2^3=82^3 - 1=7,8和7的二進位制分別為1000和0111,1000 & 0111 = 0000,也就是0嘛。
現在再來看這段程式碼( (tail = (tail + 1) & ( elements.length - 1)) == head)是不是開始理解了,(tail + 1) & ( elements.length - 1),當tail等於length-1的時候也就是(2^n) & (2^n-1),此時將結果0賦值給tail,也就是這個時候tail指向了0,印證了前面我們的說法。那麼如果tail不是陣列的最後一個位置的索引的時候呢,比如tail=5,那麼5 & (elements.length - 1)實際上就等於5對不對,因為tail永遠不會大於length的,所以當tail不等於length-1的時候,(tail + 1) & ( elements.length - 1)的結果就是tail+1(我們在HashMap中分析過h & (2^n - 1)就相當於h % 2^n)。

所以從這裡看,我們就可以將ArrayDeque看做是一個雙向迴圈佇列,之所以這裡用"看做"這個詞,是因為這裡只是程式碼邏輯上"環",而非儲存結構上的"環"。

至此,我們終於明白( (tail = (tail + 1) & ( elements.length - 1)) == head) 這句程式碼的意義,我們再來總結下這句程式碼的效果:(1)將tail+1操作,(2)如果tail+1已經越界,則將tail賦值為0,(3)當tail和head指向同一個索引時,則說明需要進行擴容。既然是需要擴容,那麼我們就來看看具體是怎麼擴容的吧。

      /**       * 陣列將要滿了的時候(tail==head)將,陣列進行2倍擴容
       */      private void doubleCapacity() {
          // 驗證head和tail是否相等 
                   assert head == tail;
          int p = head ;
          // 記錄陣列的長度 
                   int n = elements .length;
         // 計算head後面的元素個數,這裡沒有采用jdk中自帶的英文註釋right,是因為所謂佇列的上下左右,只是我們看的方位不同而已,如果上面畫的圖,這裡就應該是left而非right
                  int r = n - p; // number of elements to the right of p
         // 將陣列長度擴大2倍
                  int newCapacity = n << 1;
         // 如果此時長度小於0,則丟擲IllegalStateException異常,什麼時候newCapacity會小於0呢,前面我們說過了int值<<1越界
                  if (newCapacity < 0)
             throw new IllegalStateException( "Sorry, deque too big" );
         // 建立一個長度是原陣列大小2倍的新陣列
                  Object[] a = new Object[newCapacity];
         // 將原陣列head後的元素都拷貝值新陣列
                 System. arraycopy(elements, p, a, 0, r);
         // 將原陣列head前的元素都拷貝到新陣列
                  System. arraycopy(elements, 0, a, r, p);
         // 將新陣列賦值給elements
                  elements = (E[])a;
         // 重置head為陣列的第一個位置索引0
                  head = 0;
         // 重置tail為陣列的最後一個位置索引+1((length - 1) + 1)
                  tail = n;
     }

這裡需要清除,為什麼要進行兩次陣列copy,因為陣列被head分成了兩段。。。後面有元素,前面也有元素。。。

1.5 出隊(移除並返回隊頭元素)

      /**       * 移除並返回佇列頭部的元素,如果佇列為空,則丟擲一個NoSuchElementException異常
       */ 
            public E remove() {
          // 呼叫removeFirst方法,移除隊頭的元素
                   return removeFirst();
      }
  
      /**
            * @throws NoSuchElementException {@inheritDoc}
      */
           public E removeFirst() {
         // 呼叫pollFirst方法,移除並返回隊頭的元素
                  E x = pollFirst();
         // 如果佇列為空,則丟擲NoSuchElementException異常
                 if (x == null)
             throw new NoSuchElementException();
         return x;
     }
  
     /**
           * 移除並返問佇列頭部的元素,如果佇列為空,則返回null
      */
           public E poll() {
         // 呼叫pollFirst方法,移除並返回隊頭的元素
                  return pollFirst();
     }
 
     public E pollFirst() {
         int h = head ;
         // 取出陣列隊頭位置的元素
                  E result = elements[h]; // Element is null if deque empty
         // 如果陣列隊頭位置沒有元素,則返回null值
                if (result == null)
             return null;
         // 將陣列隊頭位置置空,也就是刪除元素
                  elements[h] = null;     // Must null out slot
         // 將head指標往前移動一個位置
                  head = (h + 1) & (elements .length - 1);
         // 將隊頭元素返回
                  return result;
}

pollFirst中的 (h + 1) & (elements . length - 1)相比已經不用再具體解釋了吧,不懂的看看上面的解釋吧,當然這是為了處理臨界的情況。

1.6 返回隊頭元素(不刪除)

      /**      *  返回佇列頭部的元素,如果佇列為空,則丟擲一個NoSuchElementException異常
       */ 
            public E element() {
          // 呼叫getFirst方法,獲取隊頭的元素 
                   return getFirst();
      }
  
      /**
            * @throws NoSuchElementException {@inheritDoc}
      */
           public E getFirst() {
         // 取得陣列head位置的元素
                  E x = elements[head ];
         // 如果陣列head位置的元素為null,則丟擲異常
                  if (x == null)
             throw new NoSuchElementException();
         return x;
     }
 
     /**      * 返回佇列頭部的元素,如果佇列為空,則返回null
      */     public E peek() {
         // 呼叫peekFirst方法,獲取隊頭的元素
                  return peekFirst();
     }
 
     public E peekFirst() {
         // 取得陣列head位置的元素並返回
                  return elements [head]; // elements[head] is null if deque empty
}

  
ArrayDeque作為Queue的操作方法,主要的難點則在於要把ArrayDeque看成一個雙向迴圈佇列,headtail指標是如何移動的,又是如果做到的,如果還不是很明白一定要對照圖解多看幾遍,並動手做一下位移和或操作。
當然ArrayDeque作為一個雙向佇列還有一些Deque特有的方法,以及作為Stack的一些方法