【Java】Java集合之ArrayDeque了解和原理分析

学而不思则罔,思而不学则殆


先看一下ArrayDeque的结构。
在这里插入图片描述

方法总结对比

1.队列入队方法

队列尾入队

方法方法实现是否抛出异常
add内部调用addLastNullPointerException
offer内部调用offerLastNullPointerException
offerLast内部调用addLastNullPointerException
addLast实现入队逻辑NullPointerException

队列头入队

方法方法实现是否抛出异常
push内部调用addFirstNullPointerException
offerFirst内部调用addFirstNullPointerException
addFirst具体实现头部入队操作NullPointerException

2.队列出队方法

队列头出队

方法方法实现是否抛出异常
pop内部调用removeFirstNoSuchElementException
removeFirst内部调用pollFirstNoSuchElementException
poll内部调用pollFirst不抛出异常
pollFirst实现出队逻辑不抛出异常

队列尾出队

方法方法实现是否抛出异常
removeLast内部调用pollLastNoSuchElementException
pollLast实现移除队尾逻辑不抛出异常

3.获取元素

获取队列头部元素

方法方法实现是否抛出异常
peek内部调用peekFirst不抛出异常
peekFirst具体实现获取头部元素逻辑不抛出异常

获取队列尾部元素

方法方法实现是否抛出异常
peekLast具体实现获取队列尾部元素逻辑不抛出异常

测试ArrayDeque

0.辅助方法打印队列信息

//ArrayDeque.java
public class ArrayDeque<E> extends AbstractCollection<E>
                           implements Deque<E>, Cloneable, Serializable
{
    /**
     * 存储deque元素的数组。
     * deque的容量就是这个数组的长度,总是2的幂。
     * 数组永远不允许变成满的,除非是在addX方法中,当它变成满的时候会立即调整大小(参见doubleCapacity),从而避免头和尾相互绕来绕去以相等。
     * 我们还保证所有不包含deque元素的数组单元格始终为空。
     */
    transient Object[] elements; // non-private to simplify nested class access
    transient int head;
    transient int tail;
}

deque的容量就是这个数组的长度,总是2的幂。即16,32,64…

底层数据结构是一个数组+头尾下标,从而实现队列的数据结构。
通过反射打印底层数据结构的具体情况,便于我们掌握底层原理,辅助打印信息源码如下:

    static void showDeque(String tag) throws NoSuchFieldException, IllegalAccessException {
        Class<? extends Deque> aClassc = ArrayDeque.class;
        System.out.println("\n----------------------------------------");
        System.out.println(tag);
        //打印队列上层情况
        System.out.println("size:" + deque.size() + " deque:" + deque);
        Field elements = aClassc.getDeclaredField("elements");
        elements.setAccessible(true);
        Object[] objects = (Object[]) elements.get(deque);
        Field head = aClassc.getDeclaredField("head");
        head.setAccessible(true);
        Object h = head.get(deque);
        Field tail = aClassc.getDeclaredField("tail");
        tail.setAccessible(true);
        Object t = tail.get(deque);
        //通过方法,打印队列底层情况
        System.out.println("elements.length:" + objects.length + " " + Arrays.toString(objects));
        System.out.println("head:" + h + " tail:" + t);
        System.out.println("----------------------------------------\n");
    }

1.测试空集合

打印队列中为添加元素的时候的情况:

static Deque<String> deque = new ArrayDeque<>();
showDeque("空元素");

log展示如下:

----------------------------------------
空元素
size:0 deque:[]
elements.length:16 [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:0
----------------------------------------

默认数组元素长度为16,头尾下标为0;所以当头尾下标相等的时候,队列为空;

    public boolean isEmpty() {
        return head == tail;
    }
    /**
     * Constructs an empty array deque with an initial capacity sufficient to hold 16 elements.
     * 构造一个初始容量足以容纳16个元素的空数组deque。
     */
    public ArrayDeque() {
        elements = new Object[16];
    }

2.测试入队和出队

        showDeque("空元素");
        deque.add("0");
        showDeque("add 0 ");
        deque.add("1");
        showDeque("add 1");
        deque.add("2");
        showDeque("add 2");
        deque.add("3");
        showDeque("add 3");
        deque.pop();
        showDeque("pop");
----------------------------------------
add 0 
size:1 deque:[0]
elements.length:16 [0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:1
----------------------------------------


----------------------------------------
add 1
size:2 deque:[0, 1]
elements.length:16 [0, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:2
----------------------------------------


----------------------------------------
add 2
size:3 deque:[0, 1, 2]
elements.length:16 [0, 1, 2, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:3
----------------------------------------


----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------


----------------------------------------
pop
size:3 deque:[1, 2, 3]
elements.length:16 [null, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:1 tail:4
----------------------------------------

入队的时候,往tail下标位置加入元素,tail自加1
出队的时候,head下标位置元素置为null,head自加1,

查看入队源码

    /**
     * Inserts the specified element at the end of this deque.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws NullPointerException if the specified element is null
     */
    public boolean add(E e) {
        addLast(e);
        return true;
    }
    /**
     * Inserts the specified element at the end of this deque.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     * @throws NullPointerException if the specified element is null
     */
    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

add方法调用addLast方法。在addLast方法中可以看出队列元素不能为null,否则会抛出异常。先赋值队列尾部元素,然后计算新的队尾下标。在判断是否需要扩容,如果需要扩容就扩容。扩容在下一节中有细讲。

3.扩容逻辑

测试扩容

当队列快要满的时候,再入队就会发生数组扩机逻辑,一般发生扩容逻辑会导致运行效率降低一点。

扩容情况一

第一种很好理解,比如队列当前情况如下,此时要加入的元素在数组最后下标的最后一个位置,在入队15,就会发生扩容:

----------------------------------------
add 0..14
size:15 deque:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
elements.length:16 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, null]
head:0 tail:15
----------------------------------------
        deque.add("15"); //此时入队,会触发底层数组扩容
        showDeque("add 15");

扩容后的结果如下:

add 15
size:16 deque:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
elements.length:32 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:16

队列大小变成了16
数组长度变成了32,16 ==》32

扩容情况二

第二种情况,再入队一个元素即要发生扩容,但是新入队的元素不是数组下标的最后一个位置,比如:

----------------------------------------
add 21
size:15 deque:[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
elements.length:16 [16, 17, 18, 19, 20, 21, null, 7, 8, 9, 10, 11, 12, 13, 14, 15]
head:7 tail:6
----------------------------------------

此时队列长度为15,数组长度为16,在入队一个元素就会发生扩容。但是队列头下标是7,队列尾下标是6;
测试这种情况下的扩容:

        deque.add("22");
        showDeque("add 22");

结果:

----------------------------------------
add 22
size:16 deque:[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
elements.length:32 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:16
----------------------------------------

数组长度变为了32,说明发生了扩容。但是数组中的顺序发生了变化。那是怎么变化的呢?

扩容逻辑原理分析

    public boolean add(E e) {
        addLast(e);
        return true;
    }
    
     /**
     * Inserts the specified element at the end of this deque.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     * @throws NullPointerException if the specified element is null
     */
    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head) //判断当前数组是否已经满了
            doubleCapacity();//扩容逻辑
    }

add方法调用了addLast。不能添加空元素,否则会报空指针异常。
判断队列是否需要扩容的主要判断是:

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

根据前面两个扩容例子,我们测试分析一下:

扩容一分析

这是扩容前数据信息。

----------------------------------------
add 0..14
size:15 deque:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
elements.length:16 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, null]
head:0 tail:15
----------------------------------------
deque.add("15"); //此时入队,会触发底层数组扩容

按照逻辑,tail = 15 的下标添加"15"元素
tail = (tail+1)& (elements.length - 1) = 16 & 15 = 10000&1111 = 0 (保留低四位数据)
所以 tail == head .触发扩容

扩容二分析

这是扩容前数据信息。

----------------------------------------
add 21
size:15 deque:[7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
elements.length:16 [16, 17, 18, 19, 20, 21, null, 7, 8, 9, 10, 11, 12, 13, 14, 15]
head:7 tail:6
----------------------------------------
deque.add("22"); //触发扩容

按照逻辑

tail = 6 的下标添加"22"元素
tail = (tail+1)& (elements.length - 1) = 7 & 15 = 0111&1111 = 7 (保留低四位数据)
所以 tail == head .触发扩容

所有上述的两种情况都会触发扩容。
扩容逻辑源码:

    /**
     * Doubles the capacity of this deque.  Call only when full, i.e.,
     * when head and tail have wrapped around to become equal.
     */
    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; //新的数组长度 = oldLength * 2
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r); //第一次copy
        System.arraycopy(elements, 0, a, r, p); //第二次copy
        elements = a;
        head = 0;
        tail = n;
    }

图示如下:
在这里插入图片描述
开始时候如图,a数组为空,原数组已经满了,现在需要移动到新的数组上来。(head = 7 tail = 7)
第一次复制,把橙色部分移动到新的数组前面(0…r-1)
在这里插入图片描述
第二次复制,把绿色部分移动到新的数组(r,oldLength-1)
在这里插入图片描述

这样便完成了一次扩容。

4.出队逻辑

队列目前情况

比如当前队列中存在四个元素:

----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------
        deque.pop(); //出队
        showDeque("pop");

出队源码


    /**
     * Pops an element from the stack represented by this deque.  In other
     * words, removes and returns the first element of this deque.
     *
     * <p>This method is equivalent to {@link #removeFirst()}.
     *
     * @return the element at the front of this deque (which is the top
     *         of the stack represented by this deque)
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E pop() {
        return removeFirst();
    }
    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    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 = (h + 1) & (elements.length - 1);
        return result;
    }

所以最终调用的pollFirst方法,前两个方法可能会抛出异常。
在pollFirst方法中:

  1. 先把队列头部元素取出,如果头部元素为null,直接退出
  2. 在把数组头部下标置为null
  3. 计算新的头部下标(主要是考虑队列头部下标在数组末尾的情况下怎么处理的)
  4. 返回队列头部元素

结果为:

----------------------------------------
pop
size:3 deque:[1, 2, 3]
elements.length:16 [null, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:1 tail:4
----------------------------------------

5.移除队尾元素

初始队列情况

----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------

移除队尾元素源码

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeLast() {
        E x = pollLast();
        if (x == null)
            throw new NoSuchElementException();
        return x;
    }
    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;
    }

removeLast方法调用pollLast方法,pollLast实现移除逻辑,这两个方法都会抛出

6.队头插入元素

当前队列情况

----------------------------------------
add 3
size:4 deque:[0, 1, 2, 3]
elements.length:16 [0, 1, 2, 3, null, null, null, null, null, null, null, null, null, null, null, null]
head:0 tail:4
----------------------------------------

        deque.offerFirst("-1"); //队列头部插入“-1”
        showDeque("offerFirst(\"-1\")");

查看插入源码

    /**
     * Inserts the specified element at the front of this deque.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Deque#offerFirst})
     * @throws NullPointerException if the specified element is null
     */
    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }
    /**
     * Inserts the specified element at the front of this deque.
     *
     * @param e the element to add
     * @throws NullPointerException if the specified element is null
     */
    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }

addFirst插入元素后,判断数组是否需要扩容,如果需要就扩容。扩容逻辑前面已经讲过了。

结束

ArrayDeque的方法没有分析完,但是其他方法分析大同小异,只要掌握了分析方法+调试方法,很容了解其底层原理。欢迎大家一起学习进步。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值