Java集合之环形数组ArrayDeque:核心源码分析

274 阅读3分钟

ArrayDeque:环形数组

核心属性:

// 底层通过数组实现
transient Object[] elements;
​
// 这里的双指针也是经典的左闭右开式,也就是:
// - 尾指针tail,指向的是addLast时应该插入的位置
// - 头指针head,指向的是getFirst时元素的位置
// 即head指向首元素,tail指向尾元素的下一个位置// 头 指针 也可以理解为索引,数组的下标
transient int head;
// 尾 指针
transient int tail;
// 最小也是8个元素,如果new时指定了小于8的大小,也会new出一个大小为8的数组
private static final int MIN_INITIAL_CAPACITY = 8;
// 默认16个元素
public ArrayDeque() {
    elements = new Object[16];
}

ArrayDeque 的数组大小必须是2的幂

  • 首节点index:head
  • 首节点前的index,即addFirst插入的位置(h + 1) & (elements.length - 1)
  • 尾节点index:(tail - 1) & (elements.length - 1)
  • 尾节点后的index,即addLast插入的位置tail

仅仅维护了head和tail,在 add/get 时往往需要求出对应的index,并且ArrayDeque为了充分利用空间,采取了环形数组,比如head在5,那么0~4的位置都可以被addLast放进来,为了避免取模操作,提高效率,规定数组大小必须是2的幂从而使得能够利用位运算加速下标的定位。

核心方法

查:getFirst/Last,peekFirst/Last

addFirst/Last,offerFirst/Last

removeFirst/Last,pollFirst/Last

为空的情况:get抛异常,peek返回null,poll返回空,remove抛异常

offer则是直接调用了add方法。

查:

public E getFirst() {
    E result = (E) elements[head];
    if (result == null)
            throw new NoSuchElementException();
    return result;
}
public E getLast() {
    // 可以看到 length必须为2的幂
    E result = (E) elements[(tail - 1) & (elements.length - 1)];
    if (result == null)
            throw new NoSuchElementException();
    return result;
}
public E peekLast() {
        return (E) elements[(tail - 1) & (elements.length - 1)];
}

peek方法就是get方法去掉异常处理的版本!

因此peek,为null直接返回,get为null抛异常。

增:

// 插入元素不允许为null
if (e == null)
    throw new NullPointerException();
// tail向右移,遇到head,触发扩容
    public void addLast(E e) {
        elements[tail] = e;
    // 举个例子 如果head为0, 16 & 15 10000 & 01111 得到 0 此时就说明需要扩容
    // 那head非0的情况,tail会循环利用head前未被利用到的空间(循环数组),因此当head = tail才扩容
    // 
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }
// head往左移,遇到tail,触发扩容
    public void addFirst(E e) {
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }

删:

// removeLast调用的这个,result为空抛异常
    public E pollLast() {
        int t = (tail - 1) & (elements.length - 1);
        E result = (E) elements[t];
        if (result == null)
            return null;
        elements[t] = null;
        tail = t;
        return result;
    }
// removeFirst调用的这个,result为空抛异常
    public E pollFirst() {
        int h = head;
        E result = (E) elements[h];
        if (result == null)
            return null;
        elements[h] = null;    
        head = (h + 1) & (elements.length - 1);
        return result;
    }
// 还是看个例子吧
    public E removeLast() {
        E x = pollLast();
        if (x == null)
            throw new NoSuchElementException();
        return x;
    }

题外话:负数的与运算

负数在计算机中都是以补码来表示的

正数:原码 = 补码;负数:补码 = 原码取反+1。

在pollLast方法的第一行有这样一行:

int t = (tail - 1) & (elements.length - 1);

tail是可以为0的,此时就是 -1 & 11111 ,真正参与位运算的是-1的补码,即:

原码1000 0001 反码 1111 1110 补码1111 1111 此时& 0001 1111 就是 0001 1111

也就顺利得到了数组的最后一个位置 2^n - 1。

扩容机制

触发时机:当add时,发现头尾节点相遇,即 head == tail 触发扩容

两倍大小,同时重置头指针为0

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

小结

ArrayDeque底层是个环形数组,实现了Deque接口,因此适合做栈与队列。ArrayDeque与HashMap一样,底层的数组必须是2的幂,因为有位运算。无参构造默认是长度为16的数组,有参构造最小也是8。