ArrayDeque双端队列解析

368 阅读4分钟

翻OkHttp源码的过程中,发现有用到ArrayDeque双端队列,在此依据一个小例子研究一下。

// 实例化对象
ArrayDeque<String> arrayDeque = new ArrayDeque();
// 添加一个条目
arrayDeque.add("a");
arrayDeque.addFirst("b");

ArrayDeque的成员变量:

  1. elements:真实存储队列,可以看到就是一个Object[]数组
  2. head: 头部索引,初始0
  3. tail: 尾部索引,初始0

实例化走构造方法:

public ArrayDeque() {
    elements = new Object[16];
}

1.实例化elements数组,初始默认容量16。


调用add方法添加元素,添加成功后返回true:

public boolean add(E e) {
    addLast(e);
    return true;
}

进addLast方法:

public void addLast(E e) {
    // 判断空的话,直接抛异常。
    if (e == null)
        throw new NullPointerException();
    // 把新元素直接塞进末尾
    elements[tail] = e;
    // tail自加1,同时限制在数组以内,超过数组最大index时通过&运算变为0
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

双端队列之所以叫双端队列,就是因为这一步,tail最大后,加一后,转为0,像一条蛇一样。同理,在首部添加元素后,head减1,如果已经是最小值0了,就转为数组index最大值!如下: 进addFirst方法:

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    // head自减1,如果是-1的话,经过&操作,变为末尾最大index
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

当 head == tail 时,调用doubleCapacity方法,扩充容量,因为这时候,elements已经被塞满了
进 doubleCapacity 方法:

// elements已经被填满,扩容2倍
private void doubleCapacity() {
    int p = head;
    int n = elements.length;
    // head指针右侧元素数量
    int r = n - p;  
    // 新的容量直接扩充2倍
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    // 直接创建新元素数组
    Object[] a = new Object[newCapacity];
    // 将elements数组中head右侧的元素copy进新数组中
    System.arraycopy(elements, p, a, 0, r);
    // 将elements数组中tail左侧的元素copy进新数组中
    System.arraycopy(elements, 0, a, r, p);
    // elements置空
    Arrays.fill(elements, null);
    elements = a;
    head = 0;
    tail = n;
}

doubleCapacity扩容方法中,比较难理解的可能就是copy数组这块,其实画个图就好理解了,这里有几种情况:
第一种,一直调用add方法把数组塞满,从add方法可以得出,head不变,tail每次加一,最大后归0,最后,tail = head = 0。

企业微信20220211-162359@2x.png

企业微信20220211-162607@2x.png

第二种,一直调用addFirst,tail不变,一直为0,head每次减1,-1后通过&操作转为最大值,下一次继续减1,最后,head = tail = 0

企业微信20220211-161108@2x.png

企业微信20220211-161707@2x.png

企业微信20220211-161914@2x.png 第三种,既调用add又调用addFirst方法,tail每次加1,head每次减1,最后,head = tail = 某个中间值

企业微信20220211-163556@2x.png

无论哪种情况,elements向新数组copy数据时,都分成两部分进行,以head = tail 的那个中间值为界限。 明白上面的图后,基本上就明白双端队列是怎么一回事了。再看一下源码中的其他方法:

// 设定数组数量,设定的值为给定参数对应的二进制值最大值,很好理解
// 比方说给定20,20的二进制是10100,经过下列运算后就是100000,转成十进制就是32
// 再比方说给定35,二进制是100011,经过运算后就是1000000,即64

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

pollFirst和pollLast是反操作,没什么可说的。


// 移除第一遇到的某元素,从head开始找,找到为止或者找到null元素为止
public boolean removeFirstOccurrence(Object o) {
   if (o != null) {
       int mask = elements.length - 1;
       int i = head;
       for (Object x; (x = elements[i]) != null; i = (i + 1) & mask) {
           if (o.equals(x)) {
               delete(i);
               return true;
           }
       }
   }
   return false;
}

//  removeFirstOccurrence的反向操作,从末位开始找
public boolean removeLastOccurrence(Object o) {
  if (o != null) {
      int mask = elements.length - 1;
      int i = (tail - 1) & mask;
      for (Object x; (x = elements[i]) != null; i = (i - 1) & mask) {
          if (o.equals(x)) {
              delete(i);
              return true;
          }
      }
  }
  return false;
}

// 删除某pos的元素,其实就是把其他位置的元素覆盖上去,然后空出边缘的pos置null,有点像贪吃蛇,分清head和tail后很好理解
boolean delete(int i) {
    checkInvariants();
    final Object[] 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;
    }
}