在 jdk1.5 中,新增了 Queue 接口,表明一種隊列集合的實現,我們繼續來聊聊 java 集合體系中的 Queue 接口。java
Queue 接口是由大名鼎鼎的 Doug Lea 建立,中文名爲道格·利,關於這位大神,會在後期進行介紹,翻開 JDK1.8 源代碼,能夠將 Queue 接口旗下的實現類抽象成以下結構圖:算法
Queue 接口,主要實現類有:ArrayDeque、LinkedList、PriorityQueue。數組
關於 LinkedList 實現類,在以前的文章中已經有所介紹,今天我們來介紹一下 ArrayDeque 這個類,若是有理解不當之處,歡迎指正。安全
在介紹 ArrayDeque 類以前,能夠從上圖中看出,ArrayDeque 實現了 Deque 接口,Deque 是啥呢,全稱含義爲double ended queue
,即雙端隊列。Deque 接口的實現類能夠被看成 FIFO(隊列)使用,也能夠看成 LIFO(棧)來使用。數據結構
其中隊列(FIFO)表示先進先出,好比水管,先進去的水先出來;棧(LIFO)表示先進後出,好比,手槍彈夾,最後進去的子彈,最早出來。多線程
ArrayDeque 是 Deque 接口的一種具體實現,因此,既能夠當成隊列,也能夠當成棧來使用,類定義以下:併發
public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable{ }
看成爲隊列使用時,咱們會將它與 LinkedList 類來作對比,在後文,咱們會作測試類來將二者進行詳細數據對比。由於 Deque 接口繼承自 Queue接口,在這裏,咱們分別列出二者接口所定義的方法,二者內容區別以下:工具
看成爲棧使用時,不免會將它與 Java 中一個叫作 Stack 的類作比較,Stack 類的數據結構也是後進先出,能夠做爲棧來使用,咱們分別列出 Stack 類和 Deque 接口所定義的方法,二者內容區別以下:源碼分析
雖然,ArrayDeque 和 Stack 類均可以做爲棧來使用,可是 ArrayDeque 的效率要高於 Stack 類,而且功能也比 Stack 類豐富的多,當須要使用棧時,Java 已不推薦使用 Stack,而是推薦使用更高效的 ArrayDeque,次選 LinkedList 。性能
從上面兩張圖中能夠看出,Deque 總共定義了 2 組方法,添加、刪除、取值都有兩套方法,它們功能相同,區別是對失敗狀況的處理不一樣,一組方法是遇到失敗會拋異常,另外一組方法是遇到失敗會返回null
。
方法雖然定義的不少,但無非就是對容器的兩端進行添加、刪除、查詢操做,明白這一點,那麼使用起來就很簡單了。
繼續回到我們要介紹的這個 ArrayDeque 類,從名字上能夠看出 ArrayDeque 底層是經過數組實現的,爲了知足能夠同時在數組兩端插入或刪除元素的需求,該數組還必須是循環的,即循環數組,也就是說數組的任何一點均可能被看做起點或者終點。
由於是循環數組,因此 head 不必定老是指向下標爲 0 的數組元素,tail 也不必定老是比 head 大。
這一點,咱們能夠經過 ArrayDeque 源碼分析得出這些結論,打開 ArrayDeque 的源碼分析,能夠看到,主要有3個關鍵參數:
public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable{ /**用於存放數組元素*/ transient Object[] elements; /**用於指向數組中頭部下標*/ transient int head; /**用於指向數組中尾部下標*/ transient int tail; /**最小容量,必須爲2的冪次方*/ private static final int MIN_INITIAL_CAPACITY = 8; }
與此同時,ArrayDeque 提供了三個構造方法,分別是默認容量,指定容量及依據給定的集合中的元素進行建立,其中默認容量爲 16。
public ArrayDeque() { //默認初始化數組大小爲 16 elements = new Object[16]; }
指定容量初始化方法,源碼以下:
public ArrayDeque(int numElements) { //指定容量 allocateElements(numElements); }
咱們來看看指定容量調用的allocateElements
方法,源碼以下:
private void allocateElements(int numElements) { elements = new Object[calculateSize(numElements)]; }
calculateSize
方法,源碼以下:
private static int calculateSize(int numElements) { //最小容量爲 8 int initialCapacity = MIN_INITIAL_CAPACITY; //若是容量大於8,好比是2的倍數 if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1); initialCapacity |= (initialCapacity >>> 2); initialCapacity |= (initialCapacity >>> 4); initialCapacity |= (initialCapacity >>> 8); initialCapacity |= (initialCapacity >>> 16); initialCapacity++; //容量超出int 型最大範圍,直接擴容到最大容量到 2 ^ 30 if (initialCapacity < 0) initialCapacity >>>= 1; } return initialCapacity; }
ArrayDeque 默認初始化容量爲 16,若是指定容量,必須是 2 的倍數,當數組容量超過 int 型最大範圍時,直接擴容到最大容量到2 ^ 30
。
ArrayDeque,添加元素的方法有兩種,一種是經過數組尾部下標進行添加,另外一種是向數組頭部下標進行添加。兩種添加方式,按照處理方式的不一樣,一種處理方式是返回爲空,另外一種處理方式是成功返回true
,二者共性是若是添加失敗直接拋異常。
addLast 方法,表示向尾部添加元素,操做以下圖:
若是插入失敗,就失敗拋異常,同時添加的元素不能爲空null
,源碼以下:
public void addLast(E e) { //不容許放入null if (e == null) throw new NullPointerException(); elements[tail] = e;//將元素插入到尾部 //將尾部進行+1,判斷下標是否越界 if ( (tail = (tail + 1) & (elements.length - 1)) == head) //數組下標越界,進行擴容 doubleCapacity(); }
值得注意的是(tail = (tail + 1) & (elements.length - 1)) == head
這個方法,
能夠把它拆成兩個步驟,第一個步驟是計算tail
數組尾部值,等於(tail + 1) & (elements.length - 1)
,這個操做是先對尾部參數進行+1
處理,而後結合數組長度經過位運算獲得尾部值,由於elements.length
是2
的倍數,因此,位運算相似作%
獲得其他數。
假設,elements.length
等於16
,測試以下:
public static void main(String[] args) { int tail = 0; int[] elements = new int[16]; for (int i = 0; i < elements.length; i++) { tail = (tail + 1) & (elements.length - 1); System.out.println("第" + (i+1) + "次計算,結果值:" + tail); } }
輸出結果:
第1次計算,結果值:1 第2次計算,結果值:2 第3次計算,結果值:3 第4次計算,結果值:4 第5次計算,結果值:5 第6次計算,結果值:6 第7次計算,結果值:7 第8次計算,結果值:8 第9次計算,結果值:9 第10次計算,結果值:10 第11次計算,結果值:11 第12次計算,結果值:12 第13次計算,結果值:13 第14次計算,結果值:14 第15次計算,結果值:15 第16次計算,結果值:0
尾部下標從一、二、三、.....、1四、1五、0,依次按照順序存儲,當達到最大值以後,返回到頭部,從 0 開始,結果是一個循環下標。
第二個步驟是判斷tail == head
是否相等,當計算處理的尾部下標循環到與頭部下標重合的時候,說明數組長度已經裝滿,直接進行擴容處理。
咱們來看看doubleCapacity()
擴容這個方法,其邏輯是申請一個更大的數組(原數組的兩倍),而後將原數組複製過去,流程圖下:
doubleCapacity()
擴容源碼以下:
private void doubleCapacity() { //擴容時頭部索引和尾部索引確定相等 assert head == tail; int p = head; int n = elements.length; //計算頭部索引到數組末端(length-1處)共有多少元素 int r = n - p; //容量翻倍,至關於 2 * n 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; }
複製數組分兩次進行,第一次複製 head 頭部索引至數組末端的元素到新數組,第二次複製 head 左邊的元素到新數組。
offerLast 方法,調用了addLast()
方法,二者不一樣之處,offerLast 有返回值,若是添加成功,則返回true
,反之,拋異常;而 addLast 無返回值。
offerLast 方法源碼以下:
public boolean offerLast(E e) { addLast(e); return true; }
addFirst 方法,與addLast()
方法同樣,都是向數組中添加元素,不一樣的是,addFirst 方法是向頭部添加元素,與 addLast 方法正好相反,可是算法原理是同樣。
addFirst 方法源碼以下:
public void addFirst(E e) { //不容許元素爲 null if (e == null) throw new NullPointerException(); //使用頭部參數計算下標 elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) //若是頭部與尾部重合,進行數組擴容 doubleCapacity(); }
假設elements.length
等於 16,咱們來測試一下,經過 head 計算的數組下標值,測試方法以下:
public static void main(String[] args) { int head = 0; int[] elements = new int[16]; for (int i = 0; i < elements.length; i++) { head = (head - 1) & (elements.length - 1); System.out.println("第" + (i+1) + "次計算,結果值:" + head); } }
輸出結果:
第1次計算,結果值:15 第2次計算,結果值:14 第3次計算,結果值:13 第4次計算,結果值:12 第5次計算,結果值:11 第6次計算,結果值:10 第7次計算,結果值:9 第8次計算,結果值:8 第9次計算,結果值:7 第10次計算,結果值:6 第11次計算,結果值:5 第12次計算,結果值:4 第13次計算,結果值:3 第14次計算,結果值:2 第15次計算,結果值:1 第16次計算,結果值:0
頭部計算的下標從1五、1四、1三、.....、二、一、0,依次從大到小按照順序存儲,當達到最小值以後,返回到頭部,從 0 開始,結果也是一個循環下標。
具體實現流程與addLast
流程正好相反,就再也不贅述了。
offerFirst 方法,調用了addFirst
方法,二者不一樣之處,offerFirst 有返回值,若是添加成功,則返回true
,反之,拋異常;而 addFirst 無返回值。
offerFirst 方法源碼以下:
public boolean offerFirst(E e) { addFirst(e); return true; }
ArrayDeque,刪除元素的方法有兩種,一種是經過數組尾部下標進行刪除,另外一種是經過數組頭部下標進行刪除。兩種刪除方式,按照處理方式的不一樣,一種處理方式是刪除失敗拋異常,另外一種處理方式是刪除失敗返回null
。
pollFirst 方法,表示刪除頭部元素,並返回刪除的元素。
pollFirst 方法源碼以下:
public E pollFirst() { //獲取數組頭部 int h = head; E result = (E) elements[h]; //判斷頭部元素是否爲空 if (result == null) return null; //設爲null,方便GC回收 elements[h] = null; //向上移動頭部元素 head = (h + 1) & (elements.length - 1); return result; }
pollFirst 方法是先獲取數組頭部元素,判斷元素是否存在,若是不存在,直接返回null
,若是存在,將其設爲null
,並返回元素。
removeFirst 方法,調用了pollFirst
方法,二者不一樣的是,removeFirst 方法,若是刪除元素失敗會拋異常,而 pollFirst 方法會返回null
,源碼以下:
public E removeFirst() { E x = pollFirst(); //返回爲null ,拋異常 if (x == null) throw new NoSuchElementException(); return x; }
pollLast 方法,與pollFirst
方法正好相反,對數組尾部元素進行刪除,並返回元素。
pollLast 方法,源碼以下:
public E pollLast() { //經過尾部計算數組下標 int t = (tail - 1) & (elements.length - 1); E result = (E) elements[t]; //判斷是否爲空 if (result == null) return null; //設爲null elements[t] = null; tail = t; return result; }
pollLast 方法是先經過數組尾部計算數組元素下標,判斷元素是否存在,若是不存在,直接返回null
,若是存在,將其設爲null
,並返回元素。
removeLast 方法,調用了pollLast
方法,二者不一樣的是,removeLast 方法,若是刪除元素失敗會拋異常,而 pollLast 方法會返回null
,源碼以下:
public E removeLast() { E x = pollLast(); //返回爲null ,拋異常 if (x == null) throw new NoSuchElementException(); return x; }
ArrayDeque,查詢元素的方法也有兩種,一種是經過數組尾部下標進行獲取,另外一種是經過數組頭部下標進行獲取。兩種查詢方式,按照處理方式的不一樣,一種處理方式是查詢失敗拋異常,另外一種處理方式是查詢失敗返回null
。
peekFirst 方法,表示經過數組頭部獲取數組元素,可能返回null
,源碼以下:
public E peekFirst() { //可能返回null return (E) elements[head]; }
getFirst 方法,表示經過數組頭部獲取數組元素,若是返回null
則拋異常,源碼以下:
public E getFirst() { E result = (E) elements[head]; //查詢返回null ,拋異常 if (result == null) throw new NoSuchElementException(); return result; }
peekLast 方法,表示經過數組尾部獲取數組元素,可能返回null
,源碼以下:
public E peekFirst() { //可能返回null return (E) elements[(tail - 1) & (elements.length - 1)]; }
getLast 方法,表示經過數組尾部獲取數組元素,若是返回null
則拋異常,源碼以下:
public E getLast() { //獲取數組尾部下標 E result = (E) elements[(tail - 1) & (elements.length - 1)]; //查詢返回null,拋異常 if (result == null) throw new NoSuchElementException(); return result; }
ArrayDeque 和 LinkedList 都是 Deque 接口的實現類,都具有既能夠做爲隊列,又能夠做爲棧來使用的特性,二者主要區別在於底層數據結構的不一樣。
ArrayDeque 底層數據結構是以循環數組爲基礎,而 LinkedList 底層數據結構是以循環鏈表爲基礎。理論上,鏈表在添加、刪除方面性能高於數組結構,在查詢方面數組結構性能高於鏈表結構,可是對於數組結構,若是不進行數組移動,在添加方面效率也很高。
下面,分別以10萬條數據爲基礎,經過添加、刪除,來測試二者做爲隊列、棧使用時所消耗的時間。
public static void main(String[] args) { ArrayDeque<String> arrayDeque = new ArrayDeque<>(); long addStart = System.currentTimeMillis(); //向隊列尾部插入 10W 條數據 for (int i = 0; i < 100000; i++) { arrayDeque.addLast(i + ""); } long result1 = System.currentTimeMillis() - addStart; System.out.println("向隊列尾部插入10W條數據耗時:" + result1); //獲取並刪除隊首元素 long deleteStart = System.currentTimeMillis(); while (true){ String content = arrayDeque.pollFirst(); if(content == null){ break; } } long result2 = System.currentTimeMillis() - deleteStart; System.out.println("\n從頭部刪除隊列10W條數據耗時:" + result2); System.out.println("隊列元素總數:" + arrayDeque.size()); }
輸出結果:
向隊列尾部插入10W條數據耗時:59 從隊列頭部刪除10W條數據耗時:4 隊列元素總數:0
public static void main(String[] args) { ArrayDeque<String> arrayDeque = new ArrayDeque<>(); long addStart = System.currentTimeMillis(); //向棧頂插入 10W 條數據 for (int i = 0; i < 100000; i++) { arrayDeque.addFirst(i + ""); } long result1 = System.currentTimeMillis() - addStart; System.out.println("向棧頂插入10W條數據耗時:" + result1); //獲取並刪除棧頂元素 long deleteStart = System.currentTimeMillis(); while (true){ String content = arrayDeque.pollFirst(); if(content == null){ break; } } long result2 = System.currentTimeMillis() - deleteStart; System.out.println("從棧頂刪除10W條數據耗時:" + result2); System.out.println("棧元素總數:" + arrayDeque.size()); }
輸出結果:
向棧頂插入10W條數據耗時:61 從棧頂刪除10W條數據耗時:3 棧元素總數:0
public static void main(String[] args) { LinkedList<String> linkedList = new LinkedList(); long addStart = System.currentTimeMillis(); //向隊列尾部插入 10W 條數據 for (int i = 0; i < 100000; i++) { linkedList.addLast(i + ""); } long result1 = System.currentTimeMillis() - addStart; System.out.println("向隊列尾部插入10W條數據耗時:" + result1); //獲取並刪除隊首元素 long deleteStart = System.currentTimeMillis(); while (true){ String content = linkedList.pollFirst(); if(content == null){ break; } } long result2 = System.currentTimeMillis() - deleteStart; System.out.println("從隊列頭部刪除10W條數據耗時:" + result2); System.out.println("隊列元素總數:" + linkedList.size()); }
輸出結果:
向隊列尾部插入10W條數據耗時:70 從隊列頭部刪除10W條數據耗時:5 隊列元素總數:0
public static void main(String[] args) { LinkedList<String> linkedList = new LinkedList(); long addStart = System.currentTimeMillis(); //向棧頂插入 10W 條數據 for (int i = 0; i < 100000; i++) { linkedList.addFirst(i + ""); } long result1 = System.currentTimeMillis() - addStart; System.out.println("向棧頂插入10W條數據耗時:" + result1); //獲取並刪除棧頂元素 long deleteStart = System.currentTimeMillis(); while (true){ String content = linkedList.pollFirst(); if(content == null){ break; } } long result2 = System.currentTimeMillis() - deleteStart; System.out.println("從棧頂刪除10W條數據耗時:" + result2); System.out.println("棧元素總數:" + linkedList.size()); }
輸出結果:
向棧頂插入10W條數據耗時:71 從棧頂刪除10W條數據耗時:5 棧元素總數:0
咱們分別以10萬條數據、100萬條數據、1000萬條數據來測試,兩個類在做爲隊列和棧方面的性能,可能由於機器的不一樣,每一個機器的測試結果不一樣,本次使用的是 mac 機器,測試結果以下圖:
從數據上能夠看出,在 10 萬條數據下,二者性能都差很少,當達到 100 萬條、1000 萬條數據的時候,二者的差異就比較明顯了,ArrayDeque 不管是做爲隊列仍是做爲棧使用,性能均高於 LinkedList 。
爲何 ArrayDeque 性能,在大數據量的時候,明顯高於 LinkedList?
我的分析,咱們曾在集合系列文章中提到過 LinkedList,LinkedList 底層是以循環鏈表來實現的,每個節點都有一個前驅、後繼的變量,也就是說,每一個節點上都存放有它上一個節點的指針和它下一個節點的指針,同時還包括它本身的元素,在同等的數據量狀況下,鏈表的內存開銷要明顯大於數組,同時由於 ArrayDeque 底層是數組結構,自然在查詢方面在優點,在插入、刪除方面,只須要移動一下頭部或者尾部變量,時間複雜度是 O(1)。
因此,在大數據量的時候,LinkedList 的內存開銷明顯大於 ArrayDeque,在插入、刪除方面,都要頻發修改節點的前驅、後繼變量;而 ArrayDeque 在插入、刪除方面依然保存高性能。
若是對於小數據量,ArrayDeque 和 LinkedList 在效率方面相差不大,可是對於大數據量,推薦使用 ArrayDeque。
ArrayDeque 底層基於循環數組實現,既能夠做爲隊列使用,又能夠做爲棧來使用。
ArrayDeque 做爲棧的時候,常常會將它與 Stack 作對比,Stack 也是一個能夠做爲棧使用的類,可是 Java 已不推薦使用它,若是要使用棧,推薦使用更高效的 ArrayDeque。
與此同時,ArrayDeque 和 LinkedList 都是 Deque 接口的實現類,二者差異在於底層數據結構的不一樣,LinkedList 底層基於循環鏈表實現,內存開銷高於 ArrayDeque,在小數據量的時候,二者效率差異不大;在大數據量的時候,ArrayDeque 性能高於 LinkedList,推薦使用 ArrayDeque 類。
還有一個不一樣的地方是,ArrayDeque 不容許插入null
,而 LinkedList 容許插入null
;同時,二者都是非線程安全的,若是在多線程環境下,建議使用 Java 併發工具包裏面的操做類。
一、JDK1.7&JDK1.8 源碼
二、知乎 - CarpenterLee -Java ArrayDeque源碼剖析
做者:炸雞可樂
原文出處:www.pzblog.cn