Java集合框架原始碼剖析:ArrayDeque

CarpenterLee發表於2016-06-16

前言

Java裡有一個叫做Stack的類,卻沒有叫做Queue的類(它是個介面名字)。當需要使用棧時,Java已不推薦使用Stack,而是推薦使用更高效的ArrayDeque;既然Queue只是一個介面,當需要使用佇列時也就首選ArrayDeque了(次選是LinkedList)。

總體介紹

要講棧和佇列,首先要講Deque介面。Deque的含義是“double ended queue”,即雙端佇列,它既可以當作棧使用,也可以當作佇列使用。下表列出了DequeQueue相對應的介面:

Queue Method Equivalent Deque Method 說明
add(e) addLast(e) 向隊尾插入元素,失敗則丟擲異常
offer(e) offerLast(e) 向隊尾插入元素,失敗則返回false
remove() removeFirst() 獲取並刪除隊首元素,失敗則丟擲異常
poll() pollFirst() 獲取並刪除隊首元素,失敗則返回null
element() getFirst() 獲取但不刪除隊首元素,失敗則丟擲異常
peek() peekFirst() 獲取但不刪除隊首元素,失敗則返回null

下表列出了DequeStack對應的介面:

Stack Method Equivalent Deque Method 說明
push(e) addFirst(e) 向棧頂插入元素,失敗則丟擲異常
offerFirst(e) 向棧頂插入元素,失敗則返回false
pop() removeFirst() 獲取並刪除棧頂元素,失敗則丟擲異常
pollFirst() 獲取並刪除棧頂元素,失敗則返回null
peek() peekFirst() 獲取但不刪除棧頂元素,失敗則丟擲異常
peekFirst() 獲取但不刪除棧頂元素,失敗則返回null

上面兩個表共定義了Deque的12個介面。新增,刪除,取值都有兩套介面,它們功能相同,區別是對失敗情況的處理不同。一套介面遇到失敗就會丟擲異常,另一套遇到失敗會返回特殊值(falsenull。除非某種實現對容量有限制,大多數情況下,新增操作是不會失敗的。雖然Deque的介面有12個之多,但無非就是對容器的兩端進行操作,或新增,或刪除,或檢視。明白了這一點講解起來就會非常簡單。

ArrayDequeLinkedListDeque的兩個通用實現,由於官方更推薦使用AarryDeque用作棧和佇列,加之上一篇已經講解過LinkedList,本文將著重講解ArrayDeque的具體實現。

從名字可以看出ArrayDeque底層通過陣列實現,為了滿足可以同時在陣列兩端插入或刪除元素的需求,該陣列還必須是迴圈的,即迴圈陣列(circular array),也就是說陣列的任何一點都可能被看作起點或者終點。ArrayDeque是非執行緒安全的(not thread-safe),當多個執行緒同時使用的時候,需要程式設計師手動同步;另外,該容器不允許放入null元素。

ArrayDeque_base.png

上圖中我們看到,head指向首端第一個有效元素,tail指向尾端第一個可以插入元素的空位。因為是迴圈陣列,所以head不一定總等於0,tail也不一定總是比head大。

方法剖析

addFirst()

addFirst(E e)的作用是在Deque的首端插入元素,也就是在head的前面插入元素,在空間足夠且下標沒有越界的情況下,只需要將elements[--head] = e即可。

ArrayDeque_addFirst.png

實際需要考慮:1.空間是否夠用,以及2.下標是否越界的問題。上圖中,如果head0之後接著呼叫addFirst(),雖然空餘空間還夠用,但head-1,下標越界了。下列程式碼很好的解決了這兩個問題。

上述程式碼我們看到,空間問題是在插入之後解決的,因為tail總是指向下一個可插入的空位,也就意味著elements陣列至少有一個空位,所以插入元素的時候不用考慮空間問題。

下標越界的處理解決起來非常簡單,head = (head - 1) & (elements.length - 1)就可以了,這段程式碼相當於取餘,同時解決了head為負值的情況。因為elements.length必需是2的指數倍,elements - 1就是二進位制低位全1,跟head - 1相與之後就起到了取模的作用,如果head - 1為負數(其實只可能是-1),則相當於對其取相對於elements.length的補碼。

下面再說說擴容函式doubleCapacity(),其邏輯是申請一個更大的陣列(原陣列的兩倍),然後將原陣列複製過去。過程如下圖所示:

ArrayDeque_doubleCapacity.png

圖中我們看到,複製分兩次進行,第一次複製head右邊的元素,第二次複製head左邊的元素。

addLast()

addLast(E e)的作用是在Deque的尾端插入元素,也就是在tail的位置插入元素,由於tail總是指向下一個可以插入的空位,因此只需要elements[tail] = e;即可。插入完成後再檢查空間,如果空間已經用光,則呼叫doubleCapacity()進行擴容。

ArrayDeque_addLast.png

下標越界處理方式addFirt()中已經講過,不再贅述。

pollFirst()

pollFirst()的作用是刪除並返回Deque首端元素,也即是head位置處的元素。如果容器不空,只需要直接返回elements[head]即可,當然還需要處理下標的問題。由於ArrayDeque中不允許放入null,當elements[head] == null時,意味著容器為空。

pollLast()

pollLast()的作用是刪除並返回Deque尾端元素,也即是tail位置前面的那個元素。

peekFirst()

peekFirst()的作用是返回但不刪除Deque首端元素,也即是head位置處的元素,直接返回elements[head]即可。

peekLast()

peekLast()的作用是返回但不刪除Deque尾端元素,也即是tail位置前面的那個元素。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

Java集合框架原始碼剖析:ArrayDeque

相關文章