二十四、剖析 ArrayDeque

本文详细解析了Java中的ArrayDeque,一种基于数组实现的双端队列,重点介绍了其循环数组结构、构造方法、添加/删除操作以及与LinkedList的区别。ArrayDeque在两端操作效率高,适合仅需Deque接口的应用场景。
摘要由CSDN通过智能技术生成

剖析 ArrayDeque

本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记

LinkedList实现了队列接口Queue和双端队列接口DequeJava容器类中还有一个双端队列的实现类ArrayDeque,它是基于数组实现的。我们知道,一般而言,由于需要移动元素,数组的插入和删除效率比较低,但ArrayDeque的效率却非常高,它是怎么实现的呢?

ArrayDeque有如下构造方法:

    /**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold 16 elements.
     */
    public ArrayDeque()

    /**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold the specified number of elements.
     *
     * @param numElements lower bound on initial capacity of the deque
     */
    public ArrayDeque(int numElements)

    /**
     * Constructs a deque containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.  (The first element returned by the collection's
     * iterator becomes the first element, or <i>front</i> of the
     * deque.)
     *
     * @param c the collection whose elements are to be placed into the deque
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayDeque(Collection<? extends E> c)

ArrayDeque实现了Deque接口,同LinkedList一样,它的队列长度也是没有限制的, Deque扩展了Queue,有队列的所有方法,还可以看作栈,有栈的基本方法push/pop/peek,还有明确的操作两端的方法如addFirst/removeLast等,具体用法与LinkedList一节介绍的类似,重要的是它实现的原理。

ArrayDeque内部主要有如下实例变量:

    /**
     * The array in which the elements of the deque are stored.
     * All array cells not holding deque elements are always null.
     * The array always has at least one null slot (at tail).
     */
    transient Object[] elements;

    /**
     * The index of the element at the head of the deque (which is the
     * element that would be removed by remove() or pop()); or an
     * arbitrary number 0 <= head < elements.length equal to tail if
     * the deque is empty.
     */
    transient int head;

    /**
     * The index at which the next element would be added to the tail
     * of the deque (via addLast(E), add(E), or push(E));
     * elements[tail] is always null.
     */
    transient int tail;

elements就是存储元素的数组。ArrayDeque的高效来源于headtail这两个变量,它们使得物理上简单的从头到尾的数组变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。接下来,我们学习一下循环数组。

3.1 循环数组

对于一般数组,比如arr,第一个元素为arr[0],最后一个为arr[arr.length-1]。但对于ArrayDeque中的数组,它是一个逻辑上的循环数组,所谓循环是指元素到数组尾之后可以接着从数组头开始,数组的长度、第一个和最后一个元素都与headtail这两个变量有关,根据headtail值之间的大小关系,可以具体分为四种情况:

  1. 如果headtail相同,则数组为空,长度为 0 0 0
  2. 如果tail大于head,则第一个元素为elements[head],最后一个为elements[tail-1],长度为tail-head,元素索引从headtail-1
  3. 如果tail小于head,且为0,则第一个元素为elements[head],最后一个为elements [elements.length-1],元素索引从headelements.length-1
  4. 如果tail小于head,且大于0,则会形成循环,第一个元素为elements[head],最后一个是elements[tail-1],元素索引从headelements.length-1,然后再从0tail-1

上面情况,根据图示查看温习一遍。

在这里插入图片描述

3.2 构造方法

默认构造方法的代码为:

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

分配了一个长度为 16 16 16​的数组。如果有参数numElements,代码为:

    public ArrayDeque(int numElements) {
        elements =
            new Object[(numElements < 1) ? 1 :
                       (numElements == Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                       numElements + 1];
    }

看最后一个构造方法:

    /**
     * Constructs a deque containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.  (The first element returned by the collection's
     * iterator becomes the first element, or <i>front</i> of the
     * deque.)
     *
     * @param c the collection whose elements are to be placed into the deque
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayDeque(Collection<? extends E> c) {
        this(c.size());
        copyElements(c);
    }

先调用ArrayDeque(int numElements)构造器,初始化elements属性,再调用copyElements函数,代码如下:

    private void copyElements(Collection<? extends E> c) {
        c.forEach(this::addLast);
    }

遍历容器c,调用addLast函数。

3.3 从尾部添加 addLast(E)

addLast(E)代码如下:

    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        final Object[] es = elements;
        es[tail] = e;
        if (head == (tail = inc(tail, es.length)))
            grow(1);
    }


首先将元素添加到tail处(es[tail] = e),然后tail指向下一个位置,调用inc(int i, int modulus)函数确定tail的下一位置,代码如下:

    /**
     * Circularly increments i, mod modulus.
     * Precondition and postcondition: 0 <= i < modulus.
     */
    static final int inc(int i, int modulus) {
        if (++i >= modulus) i = 0;
        return i;
    }

tail先加一,这时,分两种情况,如果tail大于elements的长度,那么tail的值重置为 0 0 0,否则很简单,直接加一即可。

确认tail后,通过head==tail判断数组是否满员,如果满员,则调用grow函数扩容,处理数组满员的情况,否则完成整个在尾部添加元素动作。grow函数代码如下所示:

/**
 * Increases the capacity of this deque by at least the given amount.
 *
 * @param needed the required minimum extra capacity; must be positive
 */
private void grow(int needed) {
    // overflow-conscious code
    final int oldCapacity = elements.length;
    int newCapacity;
    // Double capacity if small; else grow by 50%
    int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);
    if (jump < needed
        || (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
        newCapacity = newCapacity(needed, jump);
    final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
    // Exceptionally, here tail == head needs to be disambiguated
    if (tail < head || (tail == head && es[head] != null)) {
        // wrap around; slide first leg forward to end of array
        int newSpace = newCapacity - oldCapacity;
        System.arraycopy(es, head,
                         es, head + newSpace,
                         oldCapacity - head);
        for (int i = head, to = (head += newSpace); i < to; i++)
            es[i] = null;
    }
}

grow传入一个needed(扩张需求的数量)参数计算新容量(newCapacity)。计算newCapacity的过程中,先计算jump值,计算公式:(oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1),如果原来的长度低于 64 64 64,那么数组翻倍,否则增加 50 % 50\% 50%。得到jump值后,如果需求大于供给,即:jump < needed或者增加jump后数组的长度大于允许的最大数组长度(MAX_ARRAY_SIZE),那么调用newCapacity(int needed, int jump)函数重新计算newCapacity值,代码如下所示:

    /** Capacity calculation for edge conditions, especially overflow. */
    private int newCapacity(int needed, int jump) {
        final int oldCapacity = elements.length, minCapacity;
        if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {
            if (minCapacity < 0)
                throw new IllegalStateException("Sorry, deque too big");
            return Integer.MAX_VALUE;
        }
        if (needed > jump)
            return minCapacity;
        return (oldCapacity + jump - MAX_ARRAY_SIZE < 0)
            ? oldCapacity + jump
            : MAX_ARRAY_SIZE;
    }

代码中,先计算允许的最小容量,计算公式:minCapacity = oldCapacity + needed,即:原来的容量加上本次申请的容量。接下来需要分五种情况讨论:

  1. minCapacity > Integer.MAX_VALUE,即minCapacity溢出,那么抛出IllegalStateException("Sorry, deque too big")异常;
  2. MAX_ARRAY_SIZE < minCapacity <= Integer.MAX_VALUE,返回Integer.MAX_VALUE作为newCapacity值;
  3. minCapacity<=MAX_ARRAY_SIZE and needed > jump,返回minCapacity作为newCapacity值;
  4. minCapacity<=MAX_ARRAY_SIZE and needed <= jump and oldCapacity + jump<MAX_ARRAY_SIZE,返回oldCapacity + jump作为newCapacity值;
  5. minCapacity<=MAX_ARRAY_SIZE and needed <= jump and oldCapacity + jump >= MAX_ARRAY_SIZE,返回MAX_ARRAY_SIZE作为newCapacity值;

确认完newCapacity后,分配一个newCapacity长度的新数组es,并将elementData数组复制给es,随后将head右边的元素[head, oldCapacity),复制到[head+newSpace, newCapacity),再将[head, head+newSpace)置为null,最后更新head=head+newSpace

我们来看一个例子,例子如下图所示,添加元素 9 9 9
在这里插入图片描述

3.4 从头部添加 addFirst(E)

    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        final Object[] es = elements;
        es[head = dec(head, es.length)] = e;
        if (head == tail)
            grow(1);
    }
    static final int dec(int i, int modulus) {
        if (--i < 0) i = modulus - 1;
        return i;
    }

对比addLast(E)代码,发现两者非常相像,一个是更新tail值,一个是更新head,更新完后,后面的代码完全一样,都是先判断head==tail,如果成立,再调用grow(1),这里不重复分析。

3.5 从头部和尾部删除

从头部删除removeFirst方法的代码为:

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeFirst() {
        E e = pollFirst();
        if (e == null)
            throw new NoSuchElementException();
        return e;
    }

可知,removeFirst调用的函数是pollFirst,代码如下:

    public E pollFirst() {
        final Object[] es;
        final int h;
        E e = elementAt(es = elements, h = head);
        if (e != null) {
            es[h] = null;
            head = inc(h, es.length);
        }
        return e;
    }

代码比较简单,将原头部位置置为null,然后head置为下一个位置,下一个位置=原来head+1,如果head+1>=es.length,那么head=0,否则head=head+1

从尾部删除removeLast()调用了pollLast()函数,代码如下:

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeLast() {
        E e = pollLast();
        if (e == null)
            throw new NoSuchElementException();
        return e;
    }

    public E pollLast() {
        final Object[] es;
        final int t;
        E e = elementAt(es = elements, t = dec(tail, es.length));
        if (e != null)
            es[tail = t] = null;
        return e;
    }
    static final int dec(int i, int modulus) {
        if (--i < 0) i = modulus - 1;
        return i;
    }

代码比较简单,将原尾部位置置为null,然后tail置为上一个位置,上一个位置=原来的tail-1,如果tail-1<0,那么tail=es.length-1,否则tail=tail-1

3.6 查看长度 size()

ArrayDeque没有单独的字段维护长度,其size方法的代码如下:

    public int size() {
        return sub(tail, head, elements.length);
    }
    static final int sub(int i, int j, int modulus) {
        if ((i -= j) < 0) i += modulus;
        return i;
    }

通过该方法即可计算出size

3.7 检查给定元素是否存在

contains方法的代码为:

    public boolean contains(Object o) {
        if (o != null) {
            final Object[] es = elements;
            for (int i = head, end = tail, to = (i <= end) ? end : es.length;
                 ; i = 0, to = end) {
                for (; i < to; i++)
                    if (o.equals(es[i]))
                        return true;
                if (to == end) break;
            }
        }
        return false;
    }

如果head<=tail,那么直接遍历[head,tail),判断给定元素是否存在;否则,遍历 [ h e a d ,   e s . l e n g t h )   ⋃   [ 0 , t a i l ) [head,\ es.length)\ \bigcup\ [0,tail) [head, es.length)  [0,tail),判断给定的元素是否存在。

3.8 toArray

toArray方法toArray方法的代码为:

    public Object[] toArray() {
        return toArray(Object[].class);
    }

    private <T> T[] toArray(Class<T[]> klazz) {
        final Object[] es = elements;
        final T[] a;
        final int head = this.head, tail = this.tail, end;
        if ((end = tail + ((head <= tail) ? 0 : es.length)) >= 0) {
            // Uses null extension feature of copyOfRange
            a = Arrays.copyOfRange(es, head, end, klazz);
        } else {
            // integer overflow!
            a = Arrays.copyOfRange(es, 0, end - head, klazz);
            System.arraycopy(es, head, a, 0, es.length - head);
        }
        if (end != tail)
            System.arraycopy(es, 0, a, es.length - head, tail);
        return a;
    }

contains一样,如果head<=tail,那么直接复制[head,tail)索引元素;否则,复制 [ h e a d ,   e s . l e n g t h )   ⋃   [ 0 , t a i l ) [head,\ es.length)\ \bigcup\ [0,tail) [head, es.length)  [0,tail)索引元素。

3.9 ArrayDeque 特点分析

ArrayDeque内部维护一个动态扩展的循环数组,通过headtail变量维护数组的开始和结尾。ArrayDeque实现了双端队列,内部使用循环数组实现,这决定了它有如下特点。

  1. 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加 N N N个元素的效率为 O ( N ) O(N) O(N)
  2. 根据元素内容查找和删除的效率比较低,为 O ( N ) O(N) O(N)
  3. ArrayListLinkedList不同,没有索引位置的概念,不能根据索引位置进行操作。

ArrayDequeLinkedList都实现了Deque接口,应该用哪一个呢?如果只需要Deque接口,从两端进行操作,一般而言,ArrayDeque效率更高一些,应该被优先使用;如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选LinkedList


  1. 马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎

  2. 尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值