Java集合之ArrayDeque原始碼分析
阿新 • • 發佈:2022-05-08
一、簡介
雙端佇列是一種特殊的佇列,它的兩端都可以進出元素,故而得名雙端佇列。
ArrayDeque
是一種以陣列方式實現的雙端佇列,它是非執行緒安全的。
二、繼承體系
通過繼承體系可以看,ArrayDeque
實現了Deque
介面,Deque
介面繼承自Queue
介面,它是對Queue
的一種增強。
public interface Deque<E> extends Queue<E> { // 新增元素到佇列頭 void addFirst(E e); // 新增元素到佇列尾 void addLast(E e); // 新增元素到佇列頭 boolean offerFirst(E e); // 新增元素到佇列尾 boolean offerLast(E e); // 從佇列頭移除元素 E removeFirst(); // 從佇列尾移除元素 E removeLast(); // 從佇列頭移除元素 E pollFirst(); // 從佇列尾移除元素 E pollLast(); // 檢視佇列頭元素 E getFirst(); // 檢視佇列尾元素 E getLast(); // 檢視佇列頭元素 E peekFirst(); // 檢視佇列尾元素 E peekLast(); // 從佇列頭向後遍歷移除指定元素 boolean removeFirstOccurrence(Object o); // 從佇列尾向前遍歷移除指定元素 boolean removeLastOccurrence(Object o); // *** 佇列中的方法 *** // 新增元素,等於addLast(e) boolean add(E e); // 新增元素,等於offerLast(e) boolean offer(E e); // 移除元素,等於removeFirst() E remove(); // 移除元素,等於pollFirst() E poll(); // 檢視元素,等於getFirst() E element(); // 檢視元素,等於peekFirst() E peek(); // *** 棧方法 *** // 入棧,等於addFirst(e) void push(E e); // 出棧,等於removeFirst() E pop(); // *** Collection中的方法 *** // 刪除指定元素,等於removeFirstOccurrence(o) boolean remove(Object o); // 檢查是否包含某個元素 boolean contains(Object o); // 元素個數 public int size(); // 迭代器 Iterator<E> iterator(); // 反向迭代器 Iterator<E> descendingIterator(); }
Deque
中新增了以下幾類方法:
- *First,表示從佇列頭操作元素;
- *Last,表示從佇列尾操作元素;
-
push(e)
,pop()
,以棧的方式操作元素的方法;
原始碼分析
主要屬性
// 儲存元素的陣列
transient Object[] elements; // non-private to simplify nested class access
// 佇列頭位置
transient int head;
// 佇列尾位置
transient int tail;
// 最小初始容量
private static final int MIN_INITIAL_CAPACITY = 8;
從屬性我們可以看到,ArrayDeque
8
。
構造方法
// 預設構造方法,初始容量為16 public ArrayDeque() { elements = new Object[16]; } // 指定元素個數初始化 public ArrayDeque(int numElements) { allocateElements(numElements); } // 將集合c中的元素初始化到陣列中 public ArrayDeque(Collection<? extends E> c) { allocateElements(c.size()); addAll(c); } // 初始化陣列 private void allocateElements(int numElements) { elements = new Object[calculateSize(numElements)]; } // 計算容量,這段程式碼的邏輯是算出大於numElements的最接近的2的n次方且不小於8 // 比如,3算出來是8,9算出來是16,33算出來是64 private static int calculateSize(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 } return initialCapacity; }
通過構造方法,我們知道預設初始容量是16
,最小容量是8
。
入隊
入隊有很多方法,我們這裡主要分析兩個,addFirst(e)
和addLast(e)
。
// 從佇列頭入隊
public void addFirst(E e) {
// 不允許null元素
if (e == null)
throw new NullPointerException();
// 將head指標減1並與陣列長度減1取模
// 這是為了防止陣列到頭了邊界溢位
// 如果到頭了就從尾再向前
// 相當於迴圈利用陣列
elements[head = (head - 1) & (elements.length - 1)] = e;
// 如果頭尾挨在一起了,就擴容
// 擴容規則也很簡單,直接兩倍
if (head == tail)
doubleCapacity();
}
// 從佇列尾入隊
public void addLast(E e) {
// 不允許null元素
if (e == null)
throw new NullPointerException();
// 在尾指標的位置放入元素
// 可以看到tail指標指向的是佇列最後一個元素的下一個位置
elements[tail] = e;
// tail指標加1,如果到陣列尾了就從頭開始
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
- 入隊有兩種方式,從佇列頭或者從佇列尾;
- 如果容量不夠了,直接擴大為兩倍;
- 通過取模的方式讓頭尾指標在陣列範圍內迴圈;
-
x & (len - 1) = x % len
,使用&的方式更快;
擴容
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];
// 將舊陣列head之後的元素拷貝到新陣列中
System.arraycopy(elements, p, a, 0, r);
// 將舊陣列下標0到head之間的元素拷貝到新陣列中
System.arraycopy(elements, 0, a, r, p);
// 賦值為新陣列
elements = a;
// head指向0,tail指向舊陣列長度表示的位置
head = 0;
tail = n;
}
擴容這裡遷移元素可能有點繞,請看下面這張圖來理解。
出隊
出隊同樣有很多方法,我們主要看兩個,pollFirst()
和pollLast()
。
// 從佇列頭出隊
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
// 取佇列頭元素
E result = (E) elements[h];
// 如果佇列為空,就返回null
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];
// 如果佇列為空返回null
if (result == null)
return null;
// 將當前尾指標處置為空
elements[t] = null;
// tail指向新的尾指標處
tail = t;
// 返回取得的元素
return result;
}
- 出隊有兩種方式,從佇列頭或者從佇列尾;
- 通過取模的方式讓頭尾指標在陣列範圍內迴圈;
- 出隊之後沒有縮容哈哈^^
棧
前面我們介紹Deque
的時候說過,Deque
可以直接作為棧來使用,那麼ArrayDeque
是怎麼實現的呢?
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}
是不是很簡單,入棧出棧只要都操作佇列頭就可以了。
總結
-
ArrayDeque
是採用陣列方式實現的雙端佇列; -
ArrayDeque
的出隊入隊是通過頭尾指標迴圈利用陣列實現的; -
ArrayDeque
容量不足時是會擴容的,每次擴容容量增加一倍; -
ArrayDeque
可以直接作為棧使用;
拓展
- 雙端佇列與雙重佇列區別?
雙端佇列(Deque
)是指佇列的兩端都可以進出元素的佇列,裡面儲存的是實實在在的元素。
雙重佇列(Dual Queue
)是指一種佇列有兩種用途,裡面的節點分為資料節點和非資料節點,它是LinkedTransferQueue
使用的資料結構。