深入淺出分析 ArrayDeque

ArrayDeque 一個循環數組,誕生於 JDK 1.6,今天小編想和大家一起來揭開它的面紗!

一、摘要

在 jdk1.5 中,新增了 Queue 接口,代表一種隊列集合的實現,咱們繼續來聊聊 java 集合體系中的 Queue 接口。

Queue 接口是由大名鼎鼎的 Doug Lea 創建,中文名為道格·利,關於這位大神,會在後期進行介紹,翻開 JDK1.8 源代碼,可以將 Queue 接口旗下的實現類抽象成如下結構圖:


深入淺出分析 ArrayDeque


Queue 接口,主要實現類有:ArrayDeque、LinkedList、PriorityQueue。

關於 LinkedList 實現類,在之前的文章中已經有所介紹,今天咱們來介紹一下 ArrayDeque 這個類,如果有理解不當之處,歡迎指正。

二、簡介

在介紹 ArrayDeque 類之前,可以從上圖中看出,ArrayDeque 實現了 Deque 接口,Deque 是啥呢,全稱含義為double ended queue,即雙端隊列。Deque 接口的實現類可以被當作 FIFO(隊列)使用,也可以當作 LIFO(棧)來使用。

其中隊列(FIFO)表示先進先出,比如水管,先進去的水先出來;棧(LIFO)表示先進後出,比如,手槍彈夾,最後進去的子彈,最先出來。

ArrayDeque 是 Deque 接口的一種具體實現,所以,既可以當成隊列,也可以當成棧來使用,類定義如下:


<code>public class ArrayDeque extends AbstractCollection                           implements Deque, Cloneable, Serializable{} 
/<code>

當作為隊列使用時,我們會將它與 LinkedList 類來做對比,在後文,我們會做測試類來將兩者進行詳細數據對比。因為 Deque 接口繼承自 Queue接口,在這裡,我們分別列出兩者接口所定義的方法,兩者內容區別如下:


深入淺出分析 ArrayDeque


當作為棧使用時,難免會將它與 Java 中一個叫做 Stack 的類做比較,Stack 類的數據結構也是後進先出,可以作為棧來使用,我們分別列出 Stack 類和 Deque 接口所定義的方法,兩者內容區別如下:


深入淺出分析 ArrayDeque


雖然,ArrayDeque 和 Stack 類都可以作為棧來使用,但是 ArrayDeque 的效率要高於 Stack 類,並且功能也比 Stack 類豐富的多,當需要使用棧時,Java 已不推薦使用 Stack,而是推薦使用更高效的 ArrayDeque,次選 LinkedList

從上面兩張圖中可以看出,Deque 總共定義了 2 組方法,添加、刪除、取值都有兩套方法,它們功能相同,區別是對失敗情況的處理不同,一組方法是遇到失敗會拋異常,另一組方法是遇到失敗會返回null。

方法雖然定義的很多,但無非就是對容器的兩端進行添加、刪除、查詢操作,明白這一點,那麼使用起來就很簡單了。

繼續回到咱們要介紹的這個 ArrayDeque 類,從名字上可以看出 ArrayDeque 底層是通過數組實現的,為了滿足可以同時在數組兩端插入或刪除元素的需求,該數組還必須是循環的,即循環數組,也就是說數組的任何一點都可能被看作起點或者終點。


深入淺出分析 ArrayDeque


因為是循環數組,所以 head 不一定總是指向下標為 0 的數組元素,tail 也不一定總是比 head 大。

這一點,我們可以通過 ArrayDeque 源碼分析得出這些結論,打開 ArrayDeque 的源碼分析,可以看到,主要有3個關鍵參數:

  • elements:用於存放數組元素。
  • head:用於指向數組中頭部下標。
  • tail:用於指向數組中尾部下標。
<code>public class ArrayDeque extends AbstractCollection                           implements Deque, Cloneable, Serializable{/**用於存放數組元素*/transient Object[] elements;/**用於指向數組中頭部下標*/transient int head;/**用於指向數組中尾部下標*/transient int tail;/**最小容量,必須為2的冪次方*/private static final int MIN_INITIAL_CAPACITY = 8;}/<code>

與此同時,ArrayDeque 提供了三個構造方法,分別是默認容量,指定容量及依據給定的集合中的元素進行創建,其中默認容量為 16。

<code>public ArrayDeque() {//默認初始化數組大小為 16    elements = new Object[16];}/<code> 

指定容量初始化方法,源碼如下:

<code>public ArrayDeque(int numElements) {//指定容量allocateElements(numElements);}/<code>

我們來看看指定容量調用的allocateElements方法,源碼如下:

<code>private void allocateElements(int numElements) {elements = new Object[calculateSize(numElements)];}/<code>

calculateSize方法,源碼如下:

<code>private static int calculateSize(int numElements) {//最小容量為 8int 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;}/<code>

ArrayDeque 默認初始化容量為 16,如果指定容量,必須是 2 的倍數,當數組容量超過 int 型最大範圍時,直接擴容到最大容量到2 ^ 30。

三、常見方法介紹

3.1、添加方法

ArrayDeque,添加元素的方法有兩種,一種是通過數組尾部下標進行添加,另一種是向數組頭部下標進行添加。兩種添加方式,按照處理方式的不同,一種處理方式是返回為空,另一種處理方式是成功返回true,兩者共性是如果添加失敗直接拋異常。

3.1.1、addLast 方法

addLast 方法,表示向尾部添加元素,操作如下圖:


深入淺出分析 ArrayDeque


如果插入失敗,就失敗拋異常,同時添加的元素不能為空null,源碼如下:

<code>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();}/<code>

值得注意的是(tail = (tail + 1) & (elements.length - 1)) == head這個方法, 可以把它拆成兩個步驟,第一個步驟是計算tail數組尾部值,等於(tail + 1) & (elements.length - 1),這個操作是先對尾部參數進行+1處理,然後結合數組長度通過位運算得到尾部值,因為elements.length是2的倍數,所以,位運算類似做%得到其餘數。

假設,elements.length等於16,測試如下:

<code>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);    }}/<code>

輸出結果:

<code>第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/<code>

尾部下標從1、2、3、…..、14、15、0,依次按照順序存儲,當達到最大值之後,返回到頭部,從 0 開始,結果是一個循環下標

第二個步驟是判斷tail == head是否相等,當計算處理的尾部下標循環到與頭部下標重合的時候,說明數組長度已經裝滿,直接進行擴容處理。

我們來看看doubleCapacity()擴容這個方法,其邏輯是申請一個更大的數組(原數組的兩倍),然後將原數組複製過去,流程圖下:


深入淺出分析 ArrayDeque


doubleCapacity()擴容源碼如下:

<code>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;}/<code>

複製數組分兩次進行,第一次複製 head 頭部索引至數組末端的元素到新數組,第二次複製 head 左邊的元素到新數組。

3.1.2、offerLast 方法

offerLast 方法,調用了addLast()方法,兩者不同之處,offerLast 有返回值,如果添加成功,則返回true,反之,拋異常;而 addLast 無返回值。

offerLast 方法源碼如下:

<code>public boolean offerLast(E e) {    addLast(e);    return true;}/<code>
3.1.3、addFirst 方法

addFirst 方法,與addLast()方法一樣,都是向數組中添加元素,不同的是,addFirst 方法是向頭部添加元素,與 addLast 方法正好相反,但是算法原理是一樣。

addFirst 方法源碼如下:

<code>public void addFirst(E e) {//不允許元素為 null    if (e == null)        throw new NullPointerException();//使用頭部參數計算下標    elements[head = (head - 1) & (elements.length - 1)] = e;    if (head == tail)//如果頭部與尾部重合,進行數組擴容        doubleCapacity();}/<code>

假設elements.length等於 16,我們來測試一下,通過 head 計算的數組下標值,測試方法如下:

<code>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);    }}/<code>

輸出結果:

<code>第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/<code> 

頭部計算的下標從15、14、13、…..、2、1、0,依次從大到小按照順序存儲,當達到最小值之後,返回到頭部,從 0 開始,結果也是一個循環下標

具體實現流程與addLast流程正好相反,就不再贅述了。

3.1.4、offerFirst 方法

offerFirst 方法,調用了addFirst方法,兩者不同之處,offerFirst 有返回值,如果添加成功,則返回true,反之,拋異常;而 addFirst 無返回值。

offerFirst 方法源碼如下:

<code>public boolean offerFirst(E e) {    addFirst(e);    return true;}/<code>

3.2、刪除方法

ArrayDeque,刪除元素的方法有兩種,一種是通過數組尾部下標進行刪除,另一種是通過數組頭部下標進行刪除。兩種刪除方式,按照處理方式的不同,一種處理方式是刪除失敗拋異常,另一種處理方式是刪除失敗返回null。

3.2.1、pollFirst 方法

pollFirst 方法,表示刪除頭部元素,並返回刪除的元素。


深入淺出分析 ArrayDeque


pollFirst 方法源碼如下:

<code>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;}/<code>

pollFirst 方法是先獲取數組頭部元素,判斷元素是否存在,如果不存在,直接返回null,如果存在,將其設為null,並返回元素。

3.2.2、removeFirst 方法

removeFirst 方法,調用了pollFirst方法,兩者不同的是,removeFirst 方法,如果刪除元素失敗會拋異常,而 pollFirst 方法會返回null,源碼如下:

<code>public E removeFirst() {    E x = pollFirst();//返回為null ,拋異常    if (x == null)        throw new NoSuchElementException();    return x;}/<code>
3.2.3、pollLast 方法

pollLast 方法,與pollFirst方法正好相反,對數組尾部元素進行刪除,並返回元素。


深入淺出分析 ArrayDeque


pollLast 方法,源碼如下:

<code>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;}/<code>

pollLast 方法是先通過數組尾部計算數組元素下標,判斷元素是否存在,如果不存在,直接返回null,如果存在,將其設為null,並返回元素。

3.2.4、removeLast 方法

removeLast 方法,調用了pollLast方法,兩者不同的是,removeLast 方法,如果刪除元素失敗會拋異常,而 pollLast 方法會返回null,源碼如下:

<code>public E removeLast() {    E x = pollLast();//返回為null ,拋異常    if (x == null)        throw new NoSuchElementException();    return x;}/<code>

3.3、查詢方法

ArrayDeque,查詢元素的方法也有兩種,一種是通過數組尾部下標進行獲取,另一種是通過數組頭部下標進行獲取。兩種查詢方式,按照處理方式的不同,一種處理方式是查詢失敗拋異常,另一種處理方式是查詢失敗返回null。

3.3.1、peekFirst 方法

peekFirst 方法,表示通過數組頭部獲取數組元素,可能返回null,源碼如下:

<code>public E peekFirst() {    //可能返回null    return (E) elements[head];}/<code>
3.3.2、getFirst 方法

getFirst 方法,表示通過數組頭部獲取數組元素,如果返回null則拋異常,源碼如下:

<code>public E getFirst() {    E result = (E) elements[head];//查詢返回null ,拋異常    if (result == null)        throw new NoSuchElementException();    return result;}/<code>
3.3.3、peekLast 方法

peekLast 方法,表示通過數組尾部獲取數組元素,可能返回null,源碼如下:

<code>public E peekFirst() {    //可能返回null    return (E) elements[(tail - 1) & (elements.length - 1)];}/<code>
3.3.4、getLast 方法

getLast 方法,表示通過數組尾部獲取數組元素,如果返回null則拋異常,源碼如下:

<code>public E getLast() {//獲取數組尾部下標    E result = (E) elements[(tail - 1) & (elements.length - 1)];//查詢返回null,拋異常    if (result == null)        throw new NoSuchElementException();    return result;}/<code>

四、性能比較

ArrayDeque 和 LinkedList 都是 Deque 接口的實現類,都具備既可以作為隊列,又可以作為棧來使用的特性,兩者主要區別在於底層數據結構的不同。

ArrayDeque 底層數據結構是以循環數組為基礎,而 LinkedList 底層數據結構是以循環鏈表為基礎。理論上,鏈表在添加、刪除方面性能高於數組結構,在查詢方面數組結構性能高於鏈表結構,

但是對於數組結構,如果不進行數組移動,在添加方面效率也很高。

下面,分別以10萬條數據為基礎,通過添加、刪除,來測試兩者作為隊列、棧使用時所消耗的時間。

4.1、ArrayDeque性能測試

4.1.1、作為隊列
<code>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());}/<string>/<code>

輸出結果:

<code>向隊列尾部插入10W條數據耗時:59從隊列頭部刪除10W條數據耗時:4隊列元素總數:0/<code>
4.1.2、作為棧
<code>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());}/<string>/<code>

輸出結果:

<code>向棧頂插入10W條數據耗時:61從棧頂刪除10W條數據耗時:3棧元素總數:0/<code>

4.2、LinkedList

4.2.1、作為隊列
<code>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());}/<string>/<code>

輸出結果:


<code>向隊列尾部插入10W條數據耗時:70從隊列頭部刪除10W條數據耗時:5隊列元素總數:0/<code>
4.2.2、作為棧


<code>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());}/<string>/<code>

輸出結果:


<code>向棧頂插入10W條數據耗時:71從棧頂刪除10W條數據耗時:5棧元素總數:0/<code>

4.3、總結

我們分別以10萬條數據、100萬條數據、1000萬條數據來測試,兩個類在作為隊列和棧方面的性能,可能因為機器的不同,每個機器的測試結果不同,本次使用的是 mac 機器,測試結果如下圖:


深入淺出分析 ArrayDeque


從數據上可以看出,在 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 併發工具包裡面的操作類。

六、參考

1、JDK1.7&JDK1.8 源碼

2、知乎 - CarpenterLee -Java ArrayDeque源碼剖析


分享到:


相關文章: