【Java源碼】集合類-ArrayDeque

1、類繼承關係

ArrayDeque和LinkedList同樣都實現了雙端隊列Deque接口,但它們內部的數據結構和使用方法卻不同。根據該類的源碼註釋翻譯可知:html

  • 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的源碼。java

2、類屬性

//存儲隊列元素的數組
    //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的冪。

3、構造函數

//默認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()方法

4、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的冪呢?

下面就從主要函數中來找找答案。數組

5、如何擴容?

擴容是調用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

6、主要函數

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);
    }

7、ArrayDeque應用場景以及總結

  • 正如jdk源碼中說的「ArrayDeque看成爲棧使用時比Stack快,看成爲隊列使用時比LinkedList快。」 因此,當咱們須要使用棧這種數據結構時,優先選擇ArrayDeque,不要選擇Stack。若是做爲隊列操做首位兩端咱們應該優先選用ArrayDeque。若是須要根據索引進行操做那咱們就選擇LinkedList.
  • ArrayDeque是一個雙端隊列,也是一個棧。
  • 內部數據結構是一個動態的循環數組,head爲頭指針,tail爲尾指針
  • 內部elements數組的長度老是2的冪(目的是爲了支持位與計算,以此獲得下一個元素的位置)
  • 因爲tail始終指向下一個將被添加元素的位置,因此容量大小至少比已插入元素多一個長度。
  • 內部是一個動態的循環數組,長度是動態擴展的,因此會有額外的內存分配,以及數組複製開銷。
相關文章
相關標籤/搜索