給jdk寫註釋系列之jdk1.6容器(11)-Queue之ArrayDeque源碼解析

  前面講了Stack是一種 先進後出的數據結構:棧,那麼對應的Queue是一種 先進先出(First In First Out的數據結構:隊列。
     對比一下Stack,Queue是一種先進先出的容器,它有兩個口,從一個口放入元素,從另外一個口獲取元素。若是把棧比做一個木桶,那麼隊列就是一個管道。
  是否是很容易理解,由於隊列有兩個口,一個負責入隊另外一個負責出隊,因此會有先進先出的效果。
     固然咱們說ArrayDeque是一個雙向隊列,隊列的兩個口均可以入隊和出隊操做。再進一步說,其實ArrayDeque能夠說成是一個雙向循環隊列,是否是和鏈表的分類很像,爲何這麼說呢,咱們下面會具體分析。
 
1.定義     
1 public class ArrayDeque<E> extends AbstractCollection<E>
2                            implements Deque<E>, Cloneable, Serializable
  從ArrayDeque的定義能夠看到,它繼承AbstractCollection,實現了Deque,Cloneable,Serializable接口。不知道看到這裏你會不會發現什麼, Deque接口咱們在LinkedList中見過,LinkedList也是實現Deque接口的。咱們說過Deque是一個雙端隊列,它實現於Queue接口,什麼是雙端隊列呢,就是在隊列的同一端便可以入隊又能夠出隊,因此Deque便可以做爲隊列又能夠做爲棧使用。可是今天這裏是講Queue隊列,因此就只看單向隊列的一些原理和實現。
 
     來看下Queue接口:
 1 public interface Queue<E> extends Collection<E> {
 2     // 增長一個元素到隊尾,若是隊列已滿,則拋出一個IIIegaISlabEepeplian異常
 3     boolean add(E e);
 4     // 添加一個元素到隊尾並返回true,若是隊列已滿,則返回false
 5     boolean offer(E e);
 6     // 移除並返回隊列頭部的元素,若是隊列爲空,則拋出一個NoSuchElementException異常
 7     E remove();
 8     // 移除並返問隊列頭部的元素,若是隊列爲空,則返回null
 9     E poll();
10     // 返回隊列頭部的元素,若是隊列爲空,則拋出一個NoSuchElementException異常
11     E element();
12     // 返問隊列頭部的元素,若是隊列爲空,則返回null
13     E peek();
14 }
  看到Queue的定義,有沒有發現它和Stack的方法是很是類似的。
     可是ArrayDeque並非一個固定大小的隊列,每次隊列滿了就會進行擴容,除非擴容至超過int的邊界,纔會拋出異常。因此這裏的add和offer幾乎是沒有區別的。
 
2.底層存儲
 
     固然從ArrayDeque的命名就能夠看出他的底層是用數組實現的(而LinkedList則是用鏈表實現的隊列),來主要看一下ArrayDeque。
1     // 底層用數組存儲元素
2     private transient E[] elements;
3      // 隊列的頭部元素索引(即將pop出的一個)
4       private transient int head;
5      // 隊列下一個要添加的元素索引
6       private transient int tail;
7      // 最小的初始化容量大小,須要爲2的n次冪
8       private static final int MIN_INITIAL_CAPACITY = 8;
  這裏須要注意的是MIN_INITIAL_CAPACITY,這個初始化容量必須爲2的n次冪。爲何必需要是2的n次冪呢,還記得HashMap中咱們的分析嗎,HashMap也要求其底層數組的初始容量必須爲2的n次冪,還記得當時是基於什麼緣由嗎?不記得話,那就返回去看一下《 給jdk寫註釋系列之jdk1.6容器(4)-HashMap源碼解析》。那麼ArrayDeque這裏又是基於什麼考慮呢,咱們下面再看。
     而tail不是最後一個元素的索引,是下一個要添加的元素索引,也就是最後一個元素+1。
 
3.構造方法
 1     /**
 2      * 默認構造方法,數組的初始容量爲16
 3      */
 4     public ArrayDeque() {
 5         elements = (E[]) new Object[16];
 6     }
 7  
 8     /**
 9      * 使用一個指定的初始容量構造一個ArrayDeque
10      */
11     public ArrayDeque( int numElements) {
12         allocateElements(numElements);
13     }
14 
15     /**
16      * 構造一個指定Collection集合參數的ArrayDeque
17      */
18     public ArrayDeque(Collection<? extends E> c) {
19         allocateElements(c.size());
20         addAll(c);
21     }
22 
23     /**
24      * 分配合適容量大小的數組,確保初始容量是大於指定numElements的最小的2的n次冪
25      */
26     private void allocateElements(int numElements) {
27         int initialCapacity = MIN_INITIAL_CAPACITY;
28         // 找到大於指定容量的最小的2的n次冪
29         // Find the best power of two to hold elements.
30         // Tests "<=" because arrays aren't kept full.
31         // 若是指定的容量小於初始容量8,則執行一下if中的邏輯操做
32         if (numElements >= initialCapacity) {
33             initialCapacity = numElements;
34             initialCapacity |= (initialCapacity >>>  1);
35             initialCapacity |= (initialCapacity >>>  2);
36             initialCapacity |= (initialCapacity >>>  4);
37             initialCapacity |= (initialCapacity >>>  8);
38             initialCapacity |= (initialCapacity >>> 16);
39             initialCapacity++;
40 
41             if (initialCapacity < 0)   // Too many elements, must back off
42                 initialCapacity >>>= 1; // Good luck allocating 2 ^ 30 elements
43         }
44         elements = (E[]) new Object[initialCapacity];
45     }
  看到這裏,我相信不少人又看不懂了(包括我),可是咱們能夠來仔細分析一下,回想一下咱們在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的二進制是什麼樣子的:html

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),也就是說我怎麼作到使initialCapacity爲2^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位。來回憶一下:
1     // 確保容量爲2的n次冪,是capacity爲大於initialCapacity的最小的2的n次冪
2      int capacity = 1;
3      while (capacity < initialCapacity)
4          capacity <<= 1;

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

 

4.入隊(添加元素到隊尾)api

 1     /**
 2      * 增長一個元素,若是隊列已滿,則拋出一個IIIegaISlabEepeplian異常
 3      */
 4     public boolean add(E e) {
 5         // 調用addLast方法,將元素添加到隊尾
 6         addLast(e);
 7         return true;
 8     }
 9 
10      /**
11      * 添加一個元素
12      */
13     public boolean offer(E e) {
14         // 調用offerLast方法,將元素添加到隊尾
15         return offerLast(e);
16     }
17 
18     /**
19      * 在隊尾添加一個元素
20      */
21     public boolean offerLast(E e) {
22         // 調用addLast方法,將元素添加到隊尾
23         addLast(e);
24         return true;
25     }
26 
27     /**
28      * 將元素添加到隊尾
29      */
30     public void addLast(E e) {
31         // 若是元素爲null,咋拋出空指針異常
32         if (e == null)
33             throw new NullPointerException();
34         // 將元素e放到數組的tail位置
35         elements[tail ] = e;
36         // 判斷tail和head是否相等,若是相等則對數組進行擴容
37         if ( (tail = (tail + 1) & ( elements.length - 1)) == head)
38             // 進行兩倍擴容
39             doubleCapacity();
40     }

  這裏,( (tail = (tail + 1) & ( elements.length - 1)) == head)這句代碼是關鍵,爲何會這樣寫呢。正常的添加元素後應該是將tail+1對不對,可是隊列的刪除和添加是不在同一端的,什麼意思呢,咱們畫個圖看一下。數組

 

  咱們假設隊列的初始容量是8,初始隊列添加了4個元素A、B、C、D,分別在數組0、一、二、3的下標位置,如左圖,此時的head對應數組下標0,tail對應數組下標4。當隊列通過一系列的入隊和出隊後,就會變成右圖的樣子,此時的head對應數組下標3,tail對應數組下標7 。那麼問題來了,若是這個時候再增長一個元素到數組下標7的位置,此時理論上tail+1=8,也就是已經越界,須要對數組進行擴容了,可是咱們看下數組0、一、2的位置因爲出隊操做,這三個位置是空的,若是此時就進行擴容會形成空間的浪費。
     咱們回想一下ArrayList爲了減小空間浪費,它是怎麼作的呢,是經過數組copy,每次刪除元素都會將被刪除元素索引後面位置的元素向前移動一位。可是這樣作又形成了效率不高。
 
     怎麼辦呢,能不能換一種思路,咱們能夠 把數組想象成爲一個首尾相連的"環",數組的第一個位置索引0的位置和數組的最後一個位置索引length-1的位置是挨在一塊兒的(還記得雙向鏈表嗎?)。須要注意的是head不是數組的第一個位置索引0,tail也不是數組的最後一個位置索引length-1,head和tail其實是一個指針,隨着出隊和入隊操做不斷的移動。若是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=8和2^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指向同一個索引時,則說明須要進行擴容。既然是須要擴容,那麼咱們就來看看具體是怎麼擴容的吧。
 1     /**
 2      * 數組將要滿了的時候(tail==head)將,數組進行2倍擴容
 3      */
 4     private void doubleCapacity() {
 5         // 驗證head和tail是否相等
 6         assert head == tail;
 7         int p = head ;
 8         // 記錄數組的長度
 9         int n = elements .length;
10         // 計算head後面的元素個數,這裏沒有采用jdk中自帶的英文註釋right,是由於所謂隊列的上下左右,只是咱們看的方位不一樣而已,若是上面畫的圖,這裏就應該是left而非right
11         int r = n - p; // number of elements to the right of p
12         // 將數組長度擴大2倍
13         int newCapacity = n << 1;
14         // 若是此時長度小於0,則拋出IllegalStateException異常,何時newCapacity會小於0呢,前面咱們說過了int值<<1越界
15         if (newCapacity < 0)
16             throw new IllegalStateException( "Sorry, deque too big" );
17         // 建立一個長度是原數組大小2倍的新數組
18         Object[] a = new Object[newCapacity];
19         // 將原數組head後的元素都拷貝值新數組
20         System. arraycopy(elements, p, a, 0, r);
21         // 將原數組head前的元素都拷貝到新數組
22         System. arraycopy(elements, 0, a, r, p);
23         // 將新數組賦值給elements
24         elements = (E[])a;
25         // 重置head爲數組的第一個位置索引0
26         head = 0;
27         // 重置tail爲數組的最後一個位置索引+1((length - 1) + 1)
28         tail = n;
29     }
  這裏須要清除,爲何要進行兩次數組copy,固然是由於數組被head分紅了兩段。。。後面有元素,前面也有元素。。。
 
5.出隊(移除並返回隊頭 元素
 1     /**
 2      * 移除並返回隊列頭部的元素,若是隊列爲空,則拋出一個NoSuchElementException異常
 3      */
 4     public E remove() {
 5         // 調用removeFirst方法,移除隊頭的元素
 6         return removeFirst();
 7     }
 8 
 9     /**
10      * @throws NoSuchElementException {@inheritDoc}
11      */
12     public E removeFirst() {
13         // 調用pollFirst方法,移除並返回隊頭的元素
14         E x = pollFirst();
15         // 若是隊列爲空,則拋出NoSuchElementException異常
16         if (x == null)
17             throw new NoSuchElementException();
18         return x;
19     }
20  
21     /**
22      * 移除並返問隊列頭部的元素,若是隊列爲空,則返回null
23      */
24     public E poll() {
25         // 調用pollFirst方法,移除並返回隊頭的元素
26         return pollFirst();
27     }
28 
29     public E pollFirst() {
30         int h = head ;
31         // 取出數組隊頭位置的元素
32         E result = elements[h]; // Element is null if deque empty
33         // 若是數組隊頭位置沒有元素,則返回null值
34         if (result == null)
35             return null;
36         // 將數組隊頭位置置空,也就是刪除元素
37         elements[h] = null;     // Must null out slot
38         // 將head指針往前移動一個位置
39         head = (h + 1) & (elements .length - 1);
40         // 將隊頭元素返回
41         return result;
42     }
  pollFirst中的 (h + 1) & ( elements  .  length  - 1)相比已經不用再具體解釋了吧,不懂的看看上面的解釋吧,固然這是爲了處理臨界的狀況。
 
6.返回隊頭元素(不刪除)
 1     /**
 2      *  返回隊列頭部的元素,若是隊列爲空,則拋出一個NoSuchElementException異常
 3      */
 4     public E element() {
 5         // 調用getFirst方法,獲取隊頭的元素
 6         return getFirst();
 7     }
 8 
 9     /**
10      * @throws NoSuchElementException {@inheritDoc}
11      */
12     public E getFirst() {
13         // 取得數組head位置的元素
14         E x = elements[head ];
15         // 若是數組head位置的元素爲null,則拋出異常
16         if (x == null)
17             throw new NoSuchElementException();
18         return x;
19     }
20 
21     /**
22      * 返回隊列頭部的元素,若是隊列爲空,則返回null
23      */
24     public E peek() {
25         // 調用peekFirst方法,獲取隊頭的元素
26         return peekFirst();
27     }
28 
29     public E peekFirst() {
30         // 取得數組head位置的元素並返回
31         return elements [head]; // elements[head] is null if deque empty
32     }
  
  到此,ArrayDeque做爲Queue的操做方法,咱們就分析完了,主要的難點則在於要把ArrayDeque當作一個雙向循環隊列,head和tail指針是如何移動的,又是若是作到"環"的,若是還不是很明白必定要對照圖解多看幾遍,並動手作一下位移和或操做。
     固然ArrayDeque做爲一個雙向隊列還有一些Deque特有的方法,以及做爲Stack的一些方法,這裏咱們就很少看了,有興趣的話,能夠本身嘗試着分析下,因爲底層是數組,其餘一些操做理解起來仍是很簡單的,不太懂的能夠去回憶下《 給jdk寫註釋系列之jdk1.6容器(1)-ArrayList源碼解析》。
     固然咱們說的ArrayDeque和LinkedList都是簡單隊列(既非線程安全,又非阻塞),在java的併發包java.util.concurrent包中還有兩種隊列,併發隊列ConcurrentLinkedQueue和阻塞隊列BlockingQueue。這兩種咱們在從此分析java併發包的時候會仔細進行分析。
 
 
Queue之ArrayDeque 完!
 
 
參見:
相關文章
相關標籤/搜索