一、類繼承關係
ArrayDeque和LinkedList一樣都實現了雙端佇列Deque介面,但它們內部的資料結構和使用方法卻不一樣。根據該類的原始碼註釋翻譯可知:
- ArrayDeque實現了Deque是一個動態陣列。
- ArrayDeque沒有容量限制,容量會在使用時按需擴充套件。
- ArrayDeque不是執行緒安全的,前面一篇文章介紹Queue時提到的Java原生實現的 Stack是執行緒安全的,所以它的效能比Stack好。
- 禁止空元素。
- ArrayDeque當作為棧使用時比Stack快,當作為佇列使用時比LinkedList快。
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
所以ArrayDeque既可以作為佇列(包括雙端佇列xxxFirst,xxxLast),也可以作為棧(pop/push/peek)使用,而且它的效率也是非常高,下面就讓我們一起來讀一讀jdk1.8的原始碼。
二、類屬性
//儲存佇列元素的陣列
//power of two
transient Object[] elements;
//佇列頭部元素的索引
transient int head;
//新增一個元素的索引
transient int tail;
//最小的初始化容量(指定大小構造器使用)
private static final int MIN_INITIAL_CAPACITY = 8;
- elements是transient修飾,所以elements不能被序列化,這個和ArrayList一樣。elements陣列的容量總是2的冪。
- MIN_INITIAL_CAPACITY是呼叫指定大小構造器時使用的最小的初始化容量,這個容量是8,為2的冪。
三、建構函式
//預設16個長度
public ArrayDeque() {
elements = new Object[16];
}
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
- ArrayDeque() 無參建構函式預設新建16個長度的陣列。
- 上面第二個指定容量的建構函式,以及第三個通過Collection的建構函式都是用了allocateElements()方法
四、ArrayDeque分配空陣列
ArrayDeque通過allocateElements()方法進行擴容。下面是allocateElements()原始碼:
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];
}
- 首先將最小初始化容量8賦值給initialCapacity,通過initialCapacity和傳入的大小numElements進行比較。
- 如果傳入的容量小於8,那麼元素陣列elements的容量就是預設值8。正好是2的三次方。
- 如果傳入容量大於等於8,那麼就或通過右移(>>>)和二進位制按位或運算(|)以此使得elements內部陣列的容量為2的冪。
- 下面通過一個例項來了解大於等於8時,這段演算法內部的執行:
ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(8);
- 我們通過new一個8個容量的ArrayDeque,進入if判斷使得initialCapacity = numElements;此時initialCapacity = 8
- 然後執行 initialCapacity |= (initialCapacity >>> 1); 首先括號內的initialCapacity >>> 1 右移1位得到4,此時運算式便是initialCapacity|=4,通過二進位制按位或運算,例:a |= b ,相當於a=a | b 。得到initialCapacity=12
- initialCapacity |= (initialCapacity >>> 2);同理為12和12右移兩位結果的按位或運算,得到initialCapacity=15
- initialCapacity |= (initialCapacity >>> 4); 後面的步驟initialCapacity右移4位,8位,16位都是0,initialCapacity和0的按位或運算還是自己。最終得到所有位都變成了1,所以通過 initialCapacity++;得到二進位制數10000。容量為2的4次方。
為什麼容量必須是2的冪呢?
下面就從主要函式中來找找答案。
五、如何擴容?
擴容是呼叫doubleCapacity() 方法,當head和tail值相等時,會進行擴容,擴容大小翻倍。
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
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;
}
- int r = n - p; 計算出下面需要複製的長度
- int newCapacity = n << 1; 將原來的elements長度左移1位(乘2)
- 通過System.arraycopy(elements, p, a, 0, r); 先將head右邊的元素拷貝到新陣列a開頭處。
- System.arraycopy(elements, 0, a, r, p);再將head左邊的元素拷貝到a後面
- 最終 elements = a;設定head和tail
六、主要函式
add()/addLast(e)
通過位與計算找到下一個元素的位置。
public boolean add(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
add()函式實際上呼叫了addLast()函式,顧名思義這是將元素新增到佇列尾。前提是不能新增空元素。
- elements[tail] = e; 首先將元素新增到tail位置,第一次tail和head都為0.
- tail = (tail + 1) & (elements.length - 1) 給tail賦值,這裡先將tail指向下一個位置,也就是加一。再和elements.length - 1做位與計算。由於elements.length始終是2的冪,所以elements.length - 1的二進位制始終是111...111(每一位二進位制都是1),當(tail + 1)比(elements.length - 1)大1時得到tail為0
- (tail = (tail + 1) & (elements.length - 1)) == head 判斷tail和head相等,通過doubleCapacity()進行擴容。
例如:初始化7個容量的佇列,預設容量為8,當容量達到8時。
8 & 7 = 0 (1000 & 111)
為什麼elements.length的實際長度必須是2的冪呢?
這就是為了上面說的位與計算elements.length - 1 以此得到下一個元素的位置tail。
addFirst()
和addLast相反,新增的元素都在佇列最前面
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
- 判空
- head = (head - 1) & (elements.length - 1) 通過位與計算,計算head的值。head最開始為0,所以計算式為:
-1 & (lements.length - 1)= lements.length - 1
所以第一次新增一個元素後head就變為lements.length - 1
- 最終head == tail = 0 達到擴容的條件。
例如:
ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(7);
arrayDeque.addFirst(1);
arrayDeque.addFirst(2);
arrayDeque.addFirst(3);
執行時,ArrayDeque內部陣列結構變化為:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
3 | 2 | 1 |
第一次新增前head為0,新增時計算:head = -1 & 7 , 計算head得到7。
remove()/removeFirst()/pollFirst() 刪除第一個元素
public E remove() {
return removeFirst();
}
public E removeFirst() {
E x = pollFirst();
if (x == null)
throw new NoSuchElementException();
return x;
}
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;
}
刪除元素實際上是呼叫pollFirst()函式。
- E result = (E) elements[h]; 獲取第一個元素
- elements[h] = null; 將第一個元素置為null
head = (h + 1) & (elements.length - 1); 位與計算head移動到下一個位置
size() 檢視長度
public int size() {
return (tail - head) & (elements.length - 1);
}
七、ArrayDeque應用場景以及總結
- 正如jdk原始碼中說的“ArrayDeque當作為棧使用時比Stack快,當作為佇列使用時比LinkedList快。” 所以,當我們需要使用棧這種資料結構時,優先選擇ArrayDeque,不要選擇Stack。如果作為佇列操作首位兩端我們應該優先選用ArrayDeque。如果需要根據索引進行操作那我們就選擇LinkedList.
- ArrayDeque是一個雙端佇列,也是一個棧。
- 內部資料結構是一個動態的迴圈陣列,head為頭指標,tail為尾指標
- 內部elements陣列的長度總是2的冪(目的是為了支援位與計算,以此得到下一個元素的位置)
- 由於tail始終指向下一個將被新增元素的位置,所以容量大小至少比已插入元素多一個長度。
- 內部是一個動態的迴圈陣列,長度是動態擴充套件的,所以會有額外的記憶體分配,以及陣列複製開銷。