OpenJDK 源代閱讀之 ArrayDeque

概要

  • 類繼承關係
java.lang.Object
    java.util.AbstractCollection<E>
        java.util.ArrayDeque<E>
  • 定義
public class ArrayDeque<E>
extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
  • 要點

  1. 對 Deque 接口的實現
  2. 可調整大小
  3. 非線程安全
  4. 作爲棧比 Stack 快,作爲隊列比 LinkedList 快。
  5. 除了 remove 相關的幾個操作是線性時間,其它大部分操作均攤時間都是常數級。

實現

  • 基本數據結構
private transient E[] elements;
private transient int head;
private transient int tail;

數據用數組保存,head,tail 指向隊列頭尾。

  • allocateElements
static int allocateElements(int numElements) {
    int initialCapacity = 8;
    // 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;
}

就像註釋裏說的那樣,這個函數的目的是找到一個剛好大於 numbElements 的數,它是2的n次冪。這是通過移位及或運算的方式實現。 例如這個數爲二進制的 abcdefga 是最高的不爲0的位,即 a 爲 1,那麼第一次運算後,b 所在位一定爲 1,第二次運算後,cd 所在位一定爲 1,依此類推。最後全爲1,再加1進位。

  • doubleCapacity
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 = (E[])a;
    head = 0;
    tail = n;
}

展示瞭如何對數組進行擴充,由於這是一個雙端隊列,head 可能在隊列中間,隊列現在呈現環狀。隊列容量增加後,不能簡單地直接複製過去,而應該把 head 指向的元素放在新隊列數組的 0 號位置,其它元素依次向後排列。注意兩次System.arraycopy 分別複製了後一半,和前一半的元素。

  • addFirst
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

首先,參數不能爲空,其次, 注意 addFirst 如何巧妙地把 head 的位置後退一位,如果 head 爲0的話,& 會使得 head 變成數組最後一個位置。與循環隊列概念一致。

  • addLast
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

注意新 tail 是如何計算的。通過位運算,比通過 % 顯得高大上多了。

  • removeFirstOccurrence
public boolean removeFirstOccurrence(Object o) {
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    E x;
    while ( (x = elements[i]) != null) {
        if (o.equals(x)) {
            delete(i);
            return true;
        }
        i = (i + 1) & mask;
    }
    return false;
}

初看起來,當隊列滿時,並且找不到元素時,似乎會永遠循環下去,但是實際上,隊列永遠不會滿,所以一定會遇到 null 元素。不會滿的原因可以看下之前的 addFirst,當隊列將要滿的時候,就會自動擴充。任何一個操作之後,tail 指向的位置都爲空。

  • delete
private void checkInvariants() {
    assert elements[tail] == null;
    assert head == tail ? elements[head] == null :
        (elements[head] != null &&
         elements[(tail - 1) & (elements.length - 1)] != null);
    assert elements[(head - 1) & (elements.length - 1)] == null;
}
    private boolean delete(int i) {
    checkInvariants();
    final E[] elements = this.elements;
    final int mask = elements.length - 1;
    final int h = head;
    final int t = tail;
    final int front = (i - h) & mask;
    final int back  = (t - i) & mask;

    // Invariant: head <= i < tail mod circularity
    if (front >= ((t - h) & mask))
        throw new ConcurrentModificationException();

    // Optimize for least element motion
    if (front < back) {
        if (h <= i) {
            System.arraycopy(elements, h, elements, h + 1, front);
        } else { // Wrap around
            System.arraycopy(elements, 0, elements, 1, i);
            elements[0] = elements[mask];
            System.arraycopy(elements, h, elements, h + 1, mask - h);
        }
        elements[h] = null;
        head = (h + 1) & mask;
        return false;
    } else {
        if (i < t) { // Copy the null tail as well
            System.arraycopy(elements, i + 1, elements, i, back);
            tail = t - 1;
        } else { // Wrap around
            System.arraycopy(elements, i + 1, elements, i, mask - i);
            elements[mask] = elements[0];
            System.arraycopy(elements, 1, elements, 0, t);
            tail = (t - 1) & mask;
        }
        return true;
    }
}

首先注意 checkInvariants 的使用,它保證了執行函數前應該滿足的條件,這裏是一個前斷言。front 和 back 分別代表了要刪除的結點距離雙端隊列頭和尾的距離。通過比較 front 和 back 的大小,決定如何移動元素,使得移動次數最少。 如果front<back,還需要區分 h <= i,決定移動哪一部分。注意這是一個成環的雙端隊列,畫一下位置就明白這些操作了。主要是保證數據是連續的,head/tail 仍然保證其語義。

  • iterator

Deque 有兩個 iterator,分別從隊列頭向後遍歷,從隊列尾向前遍歷。

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();
        E result = elements[cursor];
        // This check doesn't catch all possible comodifications,
        // but does catch the ones that corrupt traversal
        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;
    }
}

這裏沒有什麼特殊的地方,就是通過 cursor 不斷指向下一個位置,一直到達尾部。

private class DescendingIterator implements Iterator<E> {
    /*
     * This class is nearly a mirror-image of DeqIterator, using
     * tail instead of head for initial cursor, and head instead of
     * tail for fence.
     */
    private int cursor = tail;
    private int fence = head;
    private int lastRet = -1;

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

    public E next() {
        if (cursor == fence)
            throw new NoSuchElementException();
        cursor = (cursor - 1) & (elements.length - 1);
        E result = elements[cursor];
        if (head != fence || result == null)
            throw new ConcurrentModificationException();
        lastRet = cursor;
        return result;
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        if (!delete(lastRet)) {
            cursor = (cursor + 1) & (elements.length - 1);
            fence = head;
        }
        lastRet = -1;
    }
}

這是從後向前的版本。

  • clear
public void clear() {
    int h = head;
    int t = tail;
    if (h != t) { // clear all cells
        head = tail = 0;
        int i = h;
        int mask = elements.length - 1;
        do {
            elements[i] = null;
            i = (i + 1) & mask;
        } while (i != t);
    }
}

clear 操作並不是直接將 head/tail 置0就好了,還需要把每個元素置爲 null

  • toArray
public <T> T[] toArray(T[] a) {
    int size = size();
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
                a.getClass().getComponentType(), size);
    copyElements(a);
    if (a.length > size)
        a[size] = null;
    return a;
}

使用反射機制,生成與泛型一致的數組,再把現有元素複製過去。

如果對代碼有更多見解,可以在這個頁面添加註釋: rtfcode-ArrayDeque

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章