Java 容器源碼分析之 Deque 與 ArrayDeque

Queue 也是 Java 集合框架中定義的一種接口,直接繼承自 Collection 接口。除了基本的 Collection 接口規定測操做外,Queue 接口還定義一組針對隊列的特殊操做。一般來講,Queue 是按照先進先出(FIFO)的方式來管理其中的元素的,可是優先隊列是一個例外。數組

Deque 接口繼承自 Queue接口,但 Deque 支持同時從兩端添加或移除元素,所以又被成爲雙端隊列。鑑於此,Deque 接口的實現能夠被看成 FIFO隊列使用,也能夠看成LIFO隊列(棧)來使用。官方也是推薦使用 Deque 的實現來替代 Stack。併發

ArrayDeque 是 Deque 接口的一種具體實現,是依賴於可變數組來實現的。ArrayDeque 沒有容量限制,可根據需求自動進行擴容。ArrayDeque不支持值爲 null 的元素。框架

下面基於JDK 8中的實現對 ArrayDeque 加以分析。ui

方法概覽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface Queue<E> extends Collection<E> {
//向隊列中插入一個元素,並返回true
//若是隊列已滿,拋出IllegalStateException異常
boolean add(E e);

//向隊列中插入一個元素,並返回true
//若是隊列已滿,返回false
boolean offer(E e);

//取出隊列頭部的元素,並從隊列中移除
//隊列爲空,拋出NoSuchElementException異常
E remove();

//取出隊列頭部的元素,並從隊列中移除
//隊列爲空,返回null
E poll();

//取出隊列頭部的元素,但並不移除
//若是隊列爲空,拋出NoSuchElementException異常
E element();

//取出隊列頭部的元素,但並不移除
//隊列爲空,返回null
E peek();
}

Deque 提供了雙端的插入與移除操做,以下表:spa

    First Element (Head)   Last Element (Tail)
  Throws exception Special value Throws exception Special value
Insert addFirst(e) offerFirst(e) addLast(e) offerLast(e)
Remove removeFirst() pollFirst() removeLast() pollLast()
Examine getFirst() peekFirst() getLast() peekLast()

Deque 和 Queue 方法的的對應關係以下:code

Queue Method Equivalent Deque Method
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

Deque 和 Stack 方法的對應關係以下:blog

Stack Method Equivalent Deque Method
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()

ArrayList 實現了 Deque 接口中的全部方法。由於 ArrayList 會根據需求自動擴充容量,於是在插入元素的時候不會拋出IllegalStateException異常。繼承

底層結構

1
2
3
4
5
6
7
8
//用數組存儲元素
transient Object[] elements; // non-private to simplify nested class access
//頭部元素的索引
transient int head;
//尾部下一個將要被加入的元素的索引
transient int tail;
//最小容量,必須爲2的冪次方
private static final int MIN_INITIAL_CAPACITY = 8;

在 ArrayDeque 底部是使用數組存儲元素,同時還使用了兩個索引來表徵當前數組的狀態,分別是 head 和 tail。head 是頭部元素的索引,但注意 tail 不是尾部元素的索引,而是尾部元素的下一位,即下一個將要被加入的元素的索引。索引

初始化

ArrayDeque 提供了三個構造方法,分別是默認容量,指定容量及依據給定的集合中的元素進行建立。默認容量爲16。接口

1
2
3
4
5
6
7
8
9
10
11
12
public ArrayDeque() {
elements = new Object[16];
}

public ArrayDeque(int numElements) {
allocateElements(numElements);
}

public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}

ArrayDeque 對數組的大小(即隊列的容量)有特殊的要求,必須是 2^n。經過 allocateElements方法計算初始容量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void allocateElements(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
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 = new Object[initialCapacity];
}

>>>是無符號右移操做,|是位或操做,通過五次右移和位或操做能夠保證獲得大小爲2^k-1的數。看一下這個例子:

1
2
3
4
0 0 0 0 1 ? ? ? ? ? //n
0 0 0 0 1 1 ? ? ? ? //n |= n >>> 1;
0 0 0 0 1 1 1 1 ? ? //n |= n >>> 2;
0 0 0 0 1 1 1 1 1 1 //n |= n >>> 4;

在進行5次位移操做和位或操做後就能夠獲得2^k-1,最後加1便可。這個實現仍是很巧妙的。

添加元素

向末尾添加元素:

1
2
3
4
5
6
7
8
9
10
11
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
//tail 中保存的是即將加入末尾的元素的索引
elements[tail] = e;
//tail 向後移動一位
//把數組看成環形的,越界後到0索引
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
//tail 和 head相遇,空間用盡,須要擴容
doubleCapacity();
}

這段代碼中,(tail = (tail + 1) & (elements.length - 1)) == head這句有點難以理解。其實,在 ArrayDeque 中數組是看成環形來使用的,索引0看做是緊挨着索引(length-1)以後的。參考下面的圖片:

array-cycle.png

那麼爲何(tail + 1) & (elements.length - 1)就能保證按照環形取得正確的下一個索引值呢?這就和前面說到的 ArrayDeque 對容量的特殊要求有關了。下面對其正確性加以驗證:

1
2
3
4
5
length = 2^n,二進制表示爲: 第 n 位爲1,低位 (n-1位) 全爲0 
length - 1 = 2^n-1,二進制表示爲:低位(n-1位)全爲1

若是 tail + 1 <= length - 1,則位與後低 (n-1) 位保持不變,高位全爲0
若是 tail + 1 = length,則位與後低 n 全爲0,高位也全爲0,結果爲 0

可見,在容量保證爲 2^n 的狀況下,僅僅經過位與操做就能夠完成環形索引的計算,而不須要進行邊界的判斷,在實現上更爲高效。

向頭部添加元素的代碼以下:

1
2
3
4
5
6
7
public void addFirst(E e) {
if (e == null) //不支持值爲null的元素
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}

其它的諸如add,offer,offerFirst,offerLast等方法都是基於上面這兩個方法實現的,再也不贅述。

擴容

在每次添加元素後,若是頭索引和尾部索引相遇,則說明數組空間已滿,須要進行擴容操做。 ArrayDeque 每次擴容都會在原有的容量上翻倍,這也是對容量必須是2的冪次方的保證。

array-cycle-copy.PNG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void doubleCapacity() {
assert head == tail; //擴容時頭部索引和尾部索引確定相等
int p = head;
int n = elements.length;
//頭部索引到數組末端(length-1處)共有多少元素
int r = n - p; // number of elements to the right of p
//容量翻倍
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;
}

移除元素

ArrayDeque支持從頭尾兩端移除元素,remove方法是經過poll來實現的。由於是基於數組的,在瞭解了環的原理後這段代碼就比較容易理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// Element is null if deque empty
if (result == null)
return null;
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1);
return result;
}

public E pollLast() {
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
E result = (E) elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}

獲取隊頭和隊尾的元素

1
2
3
4
5
6
7
8
9
10
@SuppressWarnings("unchecked")
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}

@SuppressWarnings("unchecked")
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}

迭代器

ArrayDeque 在迭代是檢查併發修改並無使用相似於 ArrayList 等容器中使用的 modCount,而是經過尾部索引的來肯定的。具體參考 next 方法中的註釋。可是這樣不必定能保證檢測到全部的併發修改狀況,加入先移除了尾部元素,又添加了一個尾部元素,這種狀況下迭代器是無法檢測出來的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
private class DeqIterator implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
private int cursor = head;

/**
* Tail recorded at construction (also in remove), to stop
* iterator and also to check for comodification.
*/
private int fence = tail;

/**
* Index of element returned by most recent call to next.
* Reset to -1 if element is deleted by a call to remove.
*/
private int lastRet = -1;

public boolean hasNext() {
return cursor != fence;
}

public E next() {
if (cursor == fence)
throw new NoSuchElementException();
@SuppressWarnings("unchecked")
E result = (E) elements[cursor];
// This check doesn't catch all possible comodifications,
// but does catch the ones that corrupt traversal
// 若是移除了尾部元素,會致使tail != fence
// 若是移除了頭部元素,會致使 result == null
if (tail != fence || result == null)
throw new ConcurrentModificationException();
lastRet = cursor;
cursor = (cursor + 1) & (elements.length - 1);
return result;
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
if (delete(lastRet)) { // if left-shifted, undo increment in next()
cursor = (cursor - 1) & (elements.length - 1);
fence = tail;
}
lastRet = -1;
}

public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] a = elements;
int m = a.length - 1, f = fence, i = cursor;
cursor = f;
while (i != f) {
@SuppressWarnings("unchecked") E e = (E)a[i];
i = (i + 1) & m;
if (e == null)
throw new ConcurrentModificationException();
action.accept(e);
}
}
}

除了 DeqIterator,還有一個反向的迭代器 DescendingIterator,順序和 DeqIterator 相反。

小結

ArrayDeque 是 Deque 接口的一種具體實現,是依賴於可變數組來實現的。ArrayDeque 沒有容量限制,可根據需求自動進行擴容。ArrayDeque 能夠做爲棧來使用,效率要高於 Stack;ArrayDeque 也能夠做爲隊列來使用,效率相較於基於雙向鏈表的 LinkedList 也要更好一些。注意,ArrayDeque 不支持爲 null 的元素。

相關文章
相關標籤/搜索