Java | ArrayDeque源码分析

前言

ArrayDeque类是Deque接口的一种实现,它表示一个双端队列。Deque接口除了提供操作双端队列的一系列方法之外,还提供了操作栈的方法,因此ArrayDeque也可以表示一个栈。LinkedList类也是Deque接口的一种实现,它是基于双向链表实现的。ArrayDeque是基于数组实现的,当数组容量不足时,会对数组进行扩容。ArrayDeque与LinkedList一样,都是非线程安全的。

相关知识

源码分析

定义

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

ArrayDeque类继承于AbstractCollection类。AbstractCollection类提供了Collection接口的一个框架实现,以最小化实现这个接口所需的工作量。

ArrayDeque类实现了3个接口:

  • Deque 双端队列接口,提供了操作双端队列和栈的一系列方法,例如:offerFirst()、offerLast()、pollFirst()、pollLast()、peekFirst()、peekLast()、push()、pop()等方法。
  • Cloneable 可克隆标记接口,表示对象可以被克隆。
  • Serializable 序列化标记接口,表示对象可以被序列化。

对于Deque接口,实际上它提供了两套操作双端队列的方法:

offer系列add系列方法描述
offerFirst()addFirst()将指定的元素插入到队首。
offerLast()addLast()将指定的元素添加到队尾。
pollFirst()removeFirst()取出并删除队首的元素。
pollLast()removeLast()取出并删除队尾的元素。
peekFirst()getFirst()取出队首的元素,但不删除。
peekLast()getLast()取出队尾的元素,但不删除。

它们的区别是:offer系列方法在操作失败时会返回一个特殊值(根据操作的不同,为false或者null);add系列方法在操作失败时会抛出异常。

主要变量

transient Object[] elements;

transient int head;

transient int tail;

private static final int MIN_INITIAL_CAPACITY = 8;

其中:

  • elements 用来存储队列的元素。elements数组是一个动态的数组,当存储容量不足时,会进行扩容。elements数组的容量总是2的幂。
  • head 队首元素的索引。
  • tail 队尾元素的下一个索引。
  • MIN_INITIAL_CAPACITY 最小初始容量,必须是2的幂。

构造方法

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

默认构造方法构造了一个初始容量为16的空双端队列。

public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];
}

该构造方法构造了一个初始容量足够容纳指定数量的元素的空双端队列。构造方法调用allocateElements()方法为elements变量分配了一个空数组。为了保证elements数组的容量总是2的幂,allocateElements()方法调用calculateSize()静态方法计算出一个最接近同时大于指定容量的2的幂。下面具体来看calculateSize()方法是如何计算的:

private static int calculateSize(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); // 第1步
        initialCapacity |= (initialCapacity >>>  2); // 第2步
        initialCapacity |= (initialCapacity >>>  4); // 第3步
        initialCapacity |= (initialCapacity >>>  8); // 第4步
        initialCapacity |= (initialCapacity >>> 16); // 第5步
        initialCapacity++;                           // 第6步

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}

如果numElements小于MIN_INITIAL_CAPACITY,那么直接返回MIN_INITIAL_CAPACITY。反之,计算出一个最接近同时大于numElements的2的幂。

2的幂是很有 特点 的:

  • 2^n,第n位为1,第n-1到0位全部为0。
  • (2^n)-1,第n位为0,第n-1到0位全部为1。

calculateSize()方法是这样计算的:对于initialCapacity,假设它的位数为n,那么第n位必然为1。第1步计算之后,initialCapacity的第n位和第n-1位必然为1。第2步计算之后,initialCapacity的第n、n-1位和第n-2、n-3位必然为1。以此类推,当计算完第5步时,initialCapacity的第n到0位全部为1。最后进行第6步计算,initialCapacity的第n+1位为1,第n到0位全部为0。此时,initialCapacity的值为2^(n+1),是一个2的幂。最后,如果计算出的值是2^31(小于0),那么右移1位,取2^30。

public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

该构造方法构造了一个包含了指定集合元素的双端队列。

offerFirst()方法

public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}    

offerFirst()方法将指定的元素插入到队首。插入成功返回true,插入失败返回false。对于无界的双端队列,可以认为插入操作一定是成功的。因此,对于ArrayDeque,offerFirst()方法等同于addFirst()方法。唯一的区别是:offerFirst()方法有返回值(总是返回true),addFirst()方法无返回值。addFirst()方法首先检查元素是否为null。如果为null,那么抛出空指针异常;反之,将指定的元素插入到队首。

下面来分析一下队首元素的索引是如何计算出来的。正常情况下,队首元素的索引为head = head - 1。当旧head为0时,新head就为-1。数组的索引范围是0到数组容量-1,所以出现异常了。此时,elements数组的容量为2的幂就可以发挥它的特点了。

head = (head - 1) & (elements.length - 1)

假设elements数组的容量为2^n,那么(elements.length - 1)就是(2^n)-1(第n位为0,第n-1到0位全部为1)。当旧head为0时,(head - 1)就是-1(在计算机中表示为32位全部为1)。我们知道当1 & 1时等于1,其它都等于0。所以,当旧head为0时,(head - 1) & (elements.length - 1)的结果就是(elements.length - 1)。当旧head大于0时,(head - 1) & (elements.length - 1)的结果就是(head - 1)。elements数组的容量为2的幂可以保证任何一个数按位与(&)数组的容量-1的结果一定在0到数组容量-1的范围之内。这就是elements数组的容量总是2的幂的原因。

head表示队首元素的索引,tail表示队尾元素的下一个索引。当head == tail时,表示elements数组已经满了。此时,addFirst()方法调用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;
}

doubleCapacity()方法首先确定了数组的新容量。默认情况下,新数组的容量为旧数组的2倍。当新容量为负数时,抛出异常。然后新建一个Object数组。接着调用System类的arraycopy()静态方法按照队列中元素的原始顺序将旧数组中的元素拷贝到新数组中。最后更新elements、head和tail变量的值。

offerLast()方法

public boolean offerLast(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();
}    

offerLast()方法将指定的元素添加到队尾。offerLast()方法的分析类似于上面的offerFirst()方法。

pollFirst()方法

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()方法取出并删除队首的元素。head表示队首元素的索引。首先通过head取出队首的元素。如果为null,表示队列是空的,那么直接返回null。反之,将elements数组中的队首元素置为null。接着计算出新的队首元素的索引,并更新head。最后返回队首的元素。

pollLast()方法

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    E result = (E) elements[t];
    if (result == null)
        return null;
    elements[t] = null;
    tail = t;
    return result;
}

pollLast()方法取出并删除队尾的元素。tail表示队尾元素的下一个索引。首先计算出队尾元素的索引。然后通过索引取出队尾的元素。如果为null,表示队列是空的,那么直接返回null。反之,将elements数组中的队尾元素置为null。接着更新tail。最后返回队尾的元素。

peekFirst()方法

public E peekFirst() {
    // elements[head] is null if deque empty
    return (E) elements[head];
}

peekFirst()方法取出队首的元素,但不删除。head表示队首元素的索引。peekFirst()方法直接通过head取出队首的元素。

peekLast()方法

public E peekLast() {
    return (E) elements[(tail - 1) & (elements.length - 1)];
}

peekLast()方法取出队尾的元素,但不删除。tail表示队尾元素的下一个索引。首先计算出队尾元素的索引。然后通过索引取出队尾的元素。

其它方法

理解了ArrayDeque的offerFirst()、offerLast()、pollFirst()、pollLast()、peekFirst()、peekLast()方法之后,其它方法就很好理解了。

总结

  • ArrayDeque可以作为一个双端队列,还可以作为一个栈。
  • ArrayDeque是基于Object数组实现的。该数组是一个动态的数组,当存储容量不足时,会进行扩容。
  • ArrayDeque在添加元素之后会检查数组是否已满。如果数组已满,那么进行扩容。默认情况下,新数组的容量为旧数组的2倍。
  • ArrayDeque中的数组的容量总是2的幂。
  • ArrayDeque不允许元素为null。
  • ArrayDeque允许元素重复。
  • ArrayDeque是非线程安全的。

参考

  • https://docs.oracle.com/javase/8/docs/api/java/util/ArrayDeque.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值