1. 程式人生 > 其它 >Java集合之ArrayDeque原始碼分析

Java集合之ArrayDeque原始碼分析

一、簡介

雙端佇列是一種特殊的佇列,它的兩端都可以進出元素,故而得名雙端佇列。

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中新增了以下幾類方法:

  1. *First,表示從佇列頭操作元素;
  2. *Last,表示從佇列尾操作元素;
  3. 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();
}
  1. 入隊有兩種方式,從佇列頭或者從佇列尾;
  2. 如果容量不夠了,直接擴大為兩倍;
  3. 通過取模的方式讓頭尾指標在陣列範圍內迴圈;
  4. 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;
}
  1. 出隊有兩種方式,從佇列頭或者從佇列尾;
  2. 通過取模的方式讓頭尾指標在陣列範圍內迴圈;
  3. 出隊之後沒有縮容哈哈^^

前面我們介紹Deque的時候說過,Deque可以直接作為棧來使用,那麼ArrayDeque是怎麼實現的呢?

public void push(E e) {
    addFirst(e);
}

public E pop() {
    return removeFirst();
}

是不是很簡單,入棧出棧只要都操作佇列頭就可以了。

總結

  1. ArrayDeque是採用陣列方式實現的雙端佇列;
  2. ArrayDeque的出隊入隊是通過頭尾指標迴圈利用陣列實現的;
  3. ArrayDeque容量不足時是會擴容的,每次擴容容量增加一倍;
  4. ArrayDeque可以直接作為棧使用;

拓展

  1. 雙端佇列與雙重佇列區別?

雙端佇列(Deque)是指佇列的兩端都可以進出元素的佇列,裡面儲存的是實實在在的元素。

雙重佇列(Dual Queue)是指一種佇列有兩種用途,裡面的節點分為資料節點和非資料節點,它是LinkedTransferQueue使用的資料結構。