Android中的双端队列数据结构ArrayDeque原理

前言

最近有时间就看了一下Retrofit库的源码,从而学习一下这个优秀框架的编码,浏览这个框架的过程中发现Dispatcher线程池的调度器使用了ArrayDeque来存储要执行网络请求的线程AsyncCall对象,因此对这个队列底层原理产生极大兴趣,所以进行了解了一般,本文会以真实代码和图片代码的形式描述,以增加可读性。

1、Dispatcher类型中的成员变量:在这里插入图片描述

2、小结:ArrayDeque底层使用数组实现。ArrayDeque中的数组没有容量限制;它根据需要增长以支持使用。它不是线程安全的;在没有外部同步的情况下,它不支持多线程的并发访问。禁止空元素。当用作堆栈时,这个类可能比Stack快,当用作队列时可能比LinkedList快。

ArrayDeque底层使用到的核心运算符&、|、<<、>>>

数据结构往往收不了用到一些运算符,所以想要熟悉核心算法,就需要了解这些运算符的计算。

  1. |(或运算符)
    运算规则:两个二进制同一位上至少有一个1,则结果中该位为1,否则为0;
    比如:20 | 2 = 22。
    解析:00010100(十进制为20) & 00000010(十进制为2)-> 00010110(十进制为22)。

  2. &(与运算符)
    运算规则:两个二进制同一位上都是1,则结果中该位为1,否则为0;
    比如:20 & 2 = 0。
    解析:00010100(十进制为20) & 00000010(十进制为2)-> 0(十进制为0)。

  3. <<(左移运算符)
    运算规则:向左移动x位,无论正负数低位(最右边)都补 x 个 0
    比如:20 << 2 = 80
    解析:00010100(十进制为 20) -> 01010000(十进制为 80)

  4. (无符号右移运算符)
    运算规则:无符号右移 x 位,所谓无符号是与 >> x对比,该操作在移动后,无论正负数高位(最左边)都补0。
    比如:20 >>> 2 = 5
    解析:00010100(十进制为 20)-> 00000101(十进制为 5)
    比如:-20 >>> 2 = 4611686018427387899
    解析:1111111111111111111111111111111111111111111111111111111111101100(十进制为 -20) ->0011111111111111111111111111111111111111111111111111111111111011(十进制为 4611686018427387899)

ArrayDeque的创建

ArrayDeque提供三个构造方法创建对象,如下图
在这里插入图片描述
把三个方法复制出来可以发现:
//创建一个内部数组长度为16的对象

public ArrayDeque() {
        elements = new Object[16];
    }
    //创建一个内部数组长度为自定义长度的对象
    public ArrayDeque(int numElements) {
       //重新计算分配制定的数组长度
        allocateElements(numElements);
    }
     //创建一个添加外部集合数据到新队列的对象
    public ArrayDeque(Collection<? extends E> c) {
       //重新计算分配制定的数组长度
        allocateElements(c.size());
        //添加外部集合数据到队列当中
        addAll(c);
    }

创建对象的核心方法,计算双端队列数组的长度,就是如果是自定义一个队列的长度,要经过这个方法一系列运算得到2的幂次方数组长度,此方法的计算原理暂不做解释,可以自己梳理理解:
![在这里插入图片描述](https://img-blog.csdnimg.cn/8077961c36444acb854f9e2fdd9f80ec.png

ArrayDeque核心方法

1、队列头部添加数据

public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
            /** 获取数组中数据的下标运算,数组的长度就起到至关重要作用,计算头部的下标:head = (head - 1) & (elements.length - 1),该运算简明一点就是:head = (head - 1) */
        elements[head = (head - 1) & (elements.length - 1)] = e;
         //计算尾部的tail是否等于头部head,等于说明队列中的数组数据已满,需要扩容
        if (head == tail)
        //开始执行扩容方法
            doubleCapacity();
    }
2、队列尾部添加数据
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
        //直接数据加入尾部,因为tail值下标的存储空间是空的,待数据直接加入
    elements[tail] = e;
     //计算尾部的tail是否等于头部head,等于说明队列中的数组数据已满,需要扩容
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
     //开始执行扩容方法
        doubleCapacity();
}

3、队列添加数据过程中,如果判断头部head和尾部tail相等,说明数组已满需要扩容,doubleCapacity方法方法主要负责扩容的逻辑。

private void doubleCapacity() {
 		//是否成立,否则抛异常
        assert head == tail;
        //头部下标
        int p = head;
        //数组长度
        int n = elements.length;
        //计算出r:头部head下标右边的元素个数
        int r = n - p; 
        //扩容数量,n左移1位等于n乘以2的1次方,如:16X2=32
        int newCapacity = n << 1;
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        //把头部部分的数据复制到新数组的开始位置为0,然后从原数组p下标开始复制,头部往后复制r个数据到新的数组中
        //例如原数组是[a,b,c,....,1,2,3],复制后的新数组是[1,2,3,....]
        System.arraycopy(elements, p, a, 0, r);
        //把结尾部部分的数据复制到新数组的开始位置为r,然后从原数组0下标开始复制,结尾部往后复制p个数据到新的数组中
        //例如原数组是[a,b,c,....,1,2,3],由于经过上一轮复制得到的[1,2,3,....],然后再次执行复制后的新数组是[1,2,3,a,b,c....]
        System.arraycopy(elements, 0, a, r, p);
        // Android-added: Clear old array instance that's about to become eligible for GC.
        // This ensures that array elements can be eligible for garbage collection even
        // before the array itself is recognized as being eligible; the latter might
        // take a while in some GC implementations, if the array instance is longer lived
        // (its liveness rarely checked) than some of its contents.
        //清除旧数据,并回收内存
        Arrays.fill(elements, null);
        //新数组赋值
        elements = a;
        //头部下标设置为0,循环操作
        head = 0;
        //尾部为旧数组长度值
        tail = n;
    }

4、获取头部数据,添加头部数据是从右到左顺序添加的,所以下标的数组的下标是递减的,获取头部数据直接通过head下标获取,获取后,删除该下标的内存,并且head下标加1。

public E pollFirst() {
        final Object[] elements = this.elements;
        final int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result != null) {
            elements[h] = null; // Must null out slot
            head = (h + 1) & (elements.length - 1);
        }
        return result;
    }

5、获取尾部数据,tail下标指向的内存空间是空的,是待添加数据的下标,所以需要tail自减1后得到的下标,该下标数据就是尾部最后一个数据了,获取到最后一个数据后,清除该下标指向的内存空间。

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

结尾:以上只是简要讲解ArrayDeque核心代码块,该队列类的其他方法大同小异,有兴趣的小伙伴可以去看看源码。

猜你喜欢

转载自blog.csdn.net/weixin_44715716/article/details/126993532