Java容器之ArrayDeque源碼分析(你知道ArrayDeque維護循環數組的原理嗎?)

  在上一篇博客 Java容器之LinkedList源碼分析(LinkedList到底是單鏈表還是雙鏈表?) 分析了LinkedList容器的源碼,LinkedList實現了Deque接口,所以它不但是一個List容器,而且還是一個雙端隊列容器,並且是基於雙鏈表實現。在此篇博客,將分析基於(循環)數組實現的雙端隊列容器——ArrayDeque

註明:以下源碼分析都是基於jdk 1.8.0_221版本
在這裏插入圖片描述

一、ArrayDeque容器概述

  ArrayDeque類的聲明如下:

public class ArrayDeque<E> extends AbstractCollection<E>
                           implements Deque<E>, Cloneable, Serializable

在這裏插入圖片描述
  Java中的ArrayDeque容器實現了Deque接口,所以它是一個雙端隊列隊頭隊尾都可以進行出隊入隊)容器,並且是底層是通過(循環)數組來存放數據。Java中的LinkedList容器也實現了Deque接口,不過底層是通過維護鏈表保存數據。
在這裏插入圖片描述

二、ArrayDeque類的主要屬性

/**
 * 循環數組
 */
transient Object[] elements; // non-private to simplify nested class access

/**
 * 隊頭對應的下標
 */
transient int head;

/**
 * 隊尾對應的下標
 */
transient int tail;
/**
 * 數組最短的長度(數組的長度需要保持爲2的冪,主要是將求餘操作轉換爲移位操作)
 */
private static final int MIN_INITIAL_CAPACITY = 8;

三、ArrayDeque類的構造器

/**
 * 數組默認初始化長度爲16
 */
public ArrayDeque() {
    elements = new Object[16];
}

/**
 * 指定數組的長度
 */
public ArrayDeque(int numElements) {
	// 不過數組的長度需要保持爲2的次冪,調用allocateElements方法初始化(後面有介紹這個方法)
    allocateElements(numElements);
}

/**
 * 複製構造方法
 */
public ArrayDeque(Collection<? extends E> c) {
	// 先初始化數組,最小長度爲c.size(不過仍然需要轉換爲不必它小的最小2的次冪)
    allocateElements(c.size());
    addAll(c);
}

四、Deque接口中的方法實現

/**
 * 往隊頭添加元素
 */
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    // 如果隊頭在數組下標0的位置,此時需要循環使用數組,插入到尾端
    // 由於elements.length一直是2的次冪,elements.length - 1則是二進制全1的數
    // (head - 1) & (elements.length - 1) == (head - 1 + elements.length) % elements.length
    elements[head = (head - 1) & (elements.length - 1)] = e;
    // 首尾下標相遇,則當前數組已經使用完了,需要擴大(後面有介紹doubleCapacity方法)
    if (head == tail)
        doubleCapacity();
}

/**
 * 從尾端插入元素
 */
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    // 由於tail的下標是隊列尾端的下一個位置,所以可以直接賦值插入
    elements[tail] = e;
    // 然後tail往後移動一個單位,如果tail在數組的尾端,則需要循環移動到數組的前端
    // (tail + 1) & (elements.length - 1) == (tail + 1) % elements.length
    // 如果與head相遇,此時需要擴容
    if ((tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}
/**
 * 隊頭出隊元素
 */
public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // 如果隊頭爲空(整個數組爲空,此時無法出隊)
    if (result == null)
        return null;
    elements[h] = null;
    // 出隊後,head往後移動一個單位(注意循環,)
    head = (h + 1) & (elements.length - 1);
    return result;
}
/**
 * 隊尾出隊元素
 */
public E pollLast() {
	// 由於隊尾指向的真隊尾的下一個位置,所以先要前移
	// (tail - 1) & (elements.length - 1)相當於 (tail - 1 + elements.length) % elements.length
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    E result = (E) elements[t];
    // 隊尾爲空(整個數組爲空),無需進行出隊操作
    if (result == null)
        return null;
   	// elements[tail - 1] = null
    elements[t] = null;
    tail = t;
    return result;
}

/**
 * 獲取隊頭
 */
public E getFirst() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[head];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}

/**
 * 獲取隊尾
 */
public E getLast() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[(tail - 1) & (elements.length - 1)];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}
/**
 * 獲取隊頭
 */
@SuppressWarnings("unchecked")
public E peekFirst() {
    // elements[head] is null if deque empty
    return (E) elements[head];
}
/**
 * 獲取隊尾
 */
@SuppressWarnings("unchecked")
public E peekLast() {
	// 由於隊尾指向的真隊尾的下一個位置,所以先要前移tail
    return (E) elements[(tail - 1) & (elements.length - 1)];
}

/**
 * 隊頭出隊
 */
public E poll() {
    return pollFirst();
}
/**
 * 獲取隊頭
 */
public E peek() {
    return peekFirst();
}

// *** Stack methods ***

/**
 * 棧頂(隊頭)入棧
 */
public void push(E e) {
    addFirst(e);
}

/**
 * 棧頂(隊頭)出棧
 */
public E pop() {
    return removeFirst();
}

五、其他方法

1、calculateSize方法

  calculateSize方法的作用是計算大於numElements的最小2的冪,比如傳入3,返回4(22);傳入4,則返回8(23);傳入13,返回16(24)。

private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // 如果numElements < MIN_INITIAL_CAPACITY(前面介紹個這個屬性,常量8),則不用查找2的冪了,直接返回
    if (numElements >= initialCapacity) {
    	// 我們需要把initialCapacity二進制形式中第一位爲1的後面的位全部置1
    	// 比如initialCapacity = 0000 0001 1011 0111 1011 1001 1101 0110
        initialCapacity = numElements;
        // initialCapacity >>> 1 爲0000 0000 1101 1011 1101 1100 1110 1011
       	// 或操作後 initialCapacity = 0000 0001 1...(後面其實不用管)
        initialCapacity |= (initialCapacity >>>  1);
        // initialCapacity >>> 1 爲 0000 0000 011...
        // 或操作後 initialCapacity = 0000 0001 111.(後面其實不用管)
        initialCapacity |= (initialCapacity >>>  2);
        // initialCapacity >>> 1 爲 0000 0000 0111 1...
        // 或操作後 initialCapacity = 0000 0001 1111 111...(後面其實不用管)
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        // 最後 initialCapacity = 0000 0001 1111 1111 1111 1111 1111 1111
        // 加1後,initialCapacity = 0000 0010 0000 0000 0000 0000 0000 0000
        initialCapacity++;
		// 如果發生了溢出(最高位爲附標誌),則將initialCapacity置爲2 ^ 30
        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}

2、allocateElements方法

/**
 * 初始化數組
 */
private void allocateElements(int numElements) {
	// 初始化前,需要計算大於numElements的最小2的次冪(上面介紹過calculateSize方法,別跳着看。。。)
    elements = new Object[calculateSize(numElements)];
}

3、doubleCapacity方法

/**
 * 當數組沒有空位置時,擴大爲原來的2倍
 */
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    // 可能head在tail的後面,此時數組尾端的[head, elements.length-1]其實是隊列前半段
    int r = n - p;
    // 容量擴大爲原來的2倍
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    // 如果 r > 0,則先複製[head, elements.length-1]這一段
    System.arraycopy(elements, p, a, 0, r);
    // 然後複製[0, head]這一段
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

六、總結

  經過上面的分析可知,ArrayDeque``雙端隊列的實現是利用數組,並且是循環數組,比如往隊頭插入元素,隊頭已經到數組的第0個位置時,可以插入到數組尾端;同樣隊尾插入元素,也可以增長到數組前段。當數組沒有空餘位置時,此時需要擴容,將當前數組中的元素全部複製到長度爲原來2倍的數組中(效率低)。LinkedList容器基於雙鏈表維護雙端隊列`,可以在頭、尾部無限增長,我不用考慮擴容問題,所以效率較高。

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