jdk1.8集合源码分析系列-5-ArrayDeque

700 阅读4分钟

ArrayDeque在数据结构上说属于双端队列,而且既然是ArrayDeque,显然是通过数组实现的双端队列,而不是链表,那么先我们简单的了解一下数组双端队列的数据结构以及常见的操作:

队列需要我们了解的几个东西有:

  • 先进先出,这个是队列的特点,它和堆栈相反,堆栈是先进后出。
  • 队头,队尾指针,分别指向头部元素和尾部元素位置,我们常常看到的xxFirst,xxLast操作就会用到这个指针。
  • 假溢出问题,上图就演示了一个假溢出问题,虽然有存储空间,但是队尾指针超出范围,就会发生“假溢出”。

顺序循环队列就能够很好的解决“假溢出”的问题,顺序循环队列的结构如下,ArrayDeque内部也是使用的顺序循环队列。

接口继承图

ArrayDeque的父接口和父类之前都有分析过,这里不再重复分析,感兴趣的可以看:ArrayList的分析LinkedList的分析

ArrayDequey源码分析

ArrayDeque数据结构

老规矩,开始分析源码之前,需要先了解ArrayDeque的数据结构。

public class ArrayDeque<E> extends AbstractCollection<E>
                           implements Deque<E>, Cloneable, Serializable
{
    //存放元素的数组
    transient Object[] elements;

    //队列头元素的在数组的下标
    transient int head;

    //队列尾元素的在数组的下标
    transient int tail;
    
    //队列初始化的最小容量,必须是2的次方,后面会讲为什么
    private static final int MIN_INITIAL_CAPACITY = 8;
}

可以看到ArrayDeque的数据结构描述和上面的图画的几乎一样,一个数组存放队列元素,用队头指针和队尾指针指向对应的位置。

ArrayDeque的初始化逻辑

// 如果构造方法什么都不传,默认创建一个长度为16的数组队列。
public ArrayDeque() { elements = new Object[16]; } 

// 下面两个方法可以看到,最终都通过allocateElements来计算初始化的容量
public ArrayDeque(int numElements) { allocateElements(numElements); }
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

// 实现非常简单,根据calculateSize计算出的长度来决定初始化的容量
private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];
}

// 又是这样的实现!我们之前HashMap的源码实现能发现几乎一模一样的算法。
// 这个计算容量的算法是返回最接近的numElements的2的次方的值
private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // 如果容量<8,那么最小返回8,如果>=8,则计算最接近numElements的2的次方的那个值
    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;
}

关于calculateSize的实现的具体分析可以看HashMap的源码分析中关于tableSizeFor方法的分析

add和remove相关逻辑分析

//往队列头添加元素
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    //计算元素存放的位置,这个算法非常精妙,待会我们单独分析。
    elements[head = (head - 1) & (elements.length - 1)] = e;
    
    // 满足扩容条件(上面的顺序循环列表我们已经分析过了)时,进行扩容
    if (head == tail)
        doubleCapacity();
}

//往队列尾添加元素
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    
    // 满足扩容条件(上面的顺序循环列表我们已经分析过了)时,进行扩容
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

//移除队列头元素
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指针的位置
    head = (h + 1) & (elements.length - 1);
    return result;
}

我们可以看到,不管addXX还是removeXX都会重新计算head或者tail的指针位置,我们来分析一下addFirst里面的head计算方法,理解了这一个,其他都理解了。代码如下: head = (head - 1) & (elements.length - 1)

我们来分析下:

  • 当head = 0的时候,head-1=-1,二进制标识11111111,那么head-1 & (element.length - 1) 其实就等于 element.length - 1,也就是size - 1,正好对应队列头的位置。
  • 当head > 0的时候,element.length由于永远是2的次方,element.length - 1 & (head - 1) 正好等于 head - 1,也就是下一个存放元素的位置。这也就回答了element.length为什么必须是2的次方。

扩容逻辑分析

扩容方法最终都走到了这个方法-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 = a;
    head = 0;
    tail = n;
}