Android双端队列——ArrayDeque的实现&源码分析[通俗易懂]

Android双端队列——ArrayDeque的实现&源码分析[通俗易懂]本文将分析Android双端队列ArrayDeque的特性、实现及源码分析。讨论ArrayDeque的实现原理以及Android中的使用。

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

ArrayDeque介绍


ArrayDeque是一个实现了Deque接口,并且可调整大小的一个双向队列。ArrayDeque队列没有容量限制,它可以根据需要扩容。

ArrayDeque底层采用数组实现的。

ArrayDeque特性:

  • ArrayDeque是一个可扩容的双端队列。
  • 内部使用数组存储数据。
  • ArrayDeque不是线程安全的。
  • ArrayDeque禁止使用空元素。
  • ArrayDeque用作堆栈时,比Stack要快,当它用作队列时,比LinkedList要快。
  • ArrayDeque最大容量是2^30个元素。
  • ArrayDeque队列的容量必须满足size = 2的n次幂。
  • ArrayDeque有两个类属性,head和tail,两个指针,可以在数组任意一端进行数据插入或删除。

Android中的使用

我们在分析AsyncTask时,它的内部实现就用到了ArrayDeque作为一个先进先出的队列。

    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                ……
            });
            ……
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

这里使用了offer方法添加元素,使用poll方法获取元素。我们知道LinkedList也可以实现队列操作,那么为什么会使用ArrayDeque呢?它有哪些优势呢?

我们接下来分析它的实现原理。

ArrayDeque解析


首先我们来看ArrayDeque的构建。

ArrayDeque队列的容量必须满足2的n次幂,它是如何保证的呢?如果我们给它的构造函数传递一个非2次幂的值,它又是如何来调整大小的呢?

ArrayDeque的队列容量初始化原理

我们来看它的构造函数:

    public ArrayDeque() {
        elements = new Object[16];
    }
    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }
    public ArrayDeque(Collection<? extends E> c) {
        allocateElements(c.size());
        addAll(c);
    }
ArrayDeque有3个重载构造方法:
  • 无参数时,ArrayDeque的初始大小是16,该值满足2的次幂要求。
  • 参数是名为numElements的int值,它允许使用者自定义容器的初始大小,该构造函数只是调用了allocateElements(numElements),看来大小调整的秘密就在allocateElements方法中了。
  • 参数为Collection的构造方法中,也同样调用了allocateElements方法,也侧面验证了allocateElements是实现ArrayDeque大小是2的次幂的关键。

那么我们就来分析allocateElements的实现吧。

allocateElements方法实现初始化容器原始大小

ArrayDeque的构造函数中会调用allocateElements方法,来实现队列的初始化。

allocateElements的源码:

    private static final int MIN_INITIAL_CAPACITY = 8;
    private void  allocateElements(int numElements) {
        int initialCapacity = MIN_INITIAL_CAPACITY;
        // 如果队列容量size的大小,大于最小值时,需要通过位运算来设置成一个符合2的n次幂的值。例如,如果参数值是9~15之间的任意值,size(initialCapacity变量的值)的最终大小就是16,如果参数值是17~31之间的任意值,则size的最终大小是32。
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            initialCapacity |= (initialCapacity >>>  1);
            initialCapacity |= (initialCapacity >>>  2);
            initialCapacity |= (initialCapacity >>>  4);
            initialCapacity |= (initialCapacity >>>  8);
            initialCapacity |= (initialCapacity >>> 16);
            initialCapacity++;

            if (initialCapacity < 0)    // 如果值太大溢出(即小于0),需要缩小2倍
                initialCapacity >>>= 1; // 最大运行值是 2^30
        }
        elements = new Object[initialCapacity];
    }
逻辑解析:
  • 这里首先判断队列容量值numElements是否小于最小初始值,如果小于,则直接以最小初始值为size,初始化队列容器,这里最小size是8。
  • 如果大于最小初始值,则进行一系列”位“操作。
  • 初始initialCapacity的二进制位中,高位往低位找,找到第一个”1“,经过这一系列位操作之后,会把”1“之后的所有位上的值,都变成”1“。例如,initialCapacity值等于9,则位操作之前是”1001“,位操作之后变为”1111“,也就是15。
  • 位操作之后,initialCapacity自增,上例中就由15变为16了,也就符合了2的次幂规则了,其他数值也可以用此方法推算出结果。
  • 最后,如果initialCapacity值溢出,则左移一位。

ArrayDeque的扩容原理

当两个双端指针相遇时,也就是head等于tail时,则表示数组需要扩容了。扩容是通过方法doubleCapacity来实现的。

doubleCapacity方法:

    private void doubleCapacity() {
        assert head == tail; //断言
        int p = head; //临时变量p,等于head
        int n = elements.length;//扩容前的size
        int r = n - p; // head右侧元素个数
        int newCapacity = n << 1;//size扩大1倍
        if (newCapacity < 0)//溢出了,抛出错误
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];//创建新的数组,容量为原来的2倍
        System.arraycopy(elements, p, a, 0, r);//将head右侧数据从原来数组copy到新数组
        System.arraycopy(elements, 0, a, r, p);//将head左侧的数据从原数组copy到新数组
        // 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;//将扩容后的数组赋给elements
        head = 0;//设置head为0
        tail = n;//设置tail为扩容前的数组大小,作为指针起点
    }
逻辑解析:
  • 计算出head指针右侧元素个数,之后需要使用它来进行数据复制。
  • 数组容量扩大1倍,如果溢出(大于2^30),则报错。
  • 创建新的数组作为容器,然后原数组的内容copy到新数组。
  • 将原数组元素都置位null。
  • 将新数组赋给elements。
  • 设置head的值为0,tail的值为原数组的大小n。这时,新数组的[0~n)指针位均已存储元素。

下面我们再来分析ArrayDeque的几个关键方法。

队列头部添加元素addFirst

head代表了队列头当前插入点的指针,它的初始值是0。

在队列头部添加一个元素,使用addFirst方法:

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

这里的关键点就在head的计算上,head – 1 的使head得指针向左移动一位。例如,当head初始值为0时,队列容量是8时,head – 1的值就是-1,用二进制表示是1111,(elements.length – 1)的二进制表示是0111,两者按位&操作,结果就是7。这种操作结果类似于取余操作,但使用起来非常方便。

也就是说addFirst的第一个元素,在数组中的位置是elements[elements.length – 1],之后每次插入操作,head指针都会向左移动。

当head指针和tail指针相等时,表示数组容器已经满了,需要进一步扩容了,扩容调用doubleCapacity方法即可。

队列尾添加元素addLast

head代表了队列尾当前插入点的指针,它的初始值是0。

在队列尾添加一个元素,使用addLast方法:

    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }
逻辑解析:
  • 这里直接把元素存储到tail指针位置,初始时tail的值是0,也就是第一个在尾部添加的元素数组索引是0。
  • 接下来tail指针右移,指向新的位置。这里同样使用了位运算来实现,以队列容量大小8为例,当tail的位置在第八位时(tail值是7),tail的二进制是0111,加1后变为1000,按位&二进制0111(elements.length – 1的二进制),结果为0。
  • 同样,当tail等于head时,扩容。

队列头弹出元素pollFirst

    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;
    }
逻辑解析:
  • 获取head指针位置的元素:elements[h],并赋值给result变量。
  • 当元素不为null时,将elements[h]置位null,并且head指针右移(同样使用了位操作)。
  • 最后返回result变量。

队列尾弹出元素pollLast

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

逻辑解析:
  • 设置临时变量t,并将tail左移一位之后赋值给t。
  • 获取队尾元素,并赋值给result。
  • 如果result不为null,将elements[t]置位null,并且将t赋值给tail指针。
  • 最后返回result变量。

offer方法和poll方法


前文原理解析中,介绍了ArrayDeque的主要方法,但并没有AsyncTask中使用的offer方法和poll方法。其实这两个方法都是基于之前方法实现的封装接口,我们来看。

offer方法

    public boolean offer(E e) {
        return offerLast(e);
    }
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }

offer方法调用了offerLast方法,offerLast方法最终调用的是offerLast方法,offerLast方法之前已经做过分析了,它实现了在队尾添加元素。

poll方法

    public E poll() {
        return pollFirst();
    }

poll方法直接return了pollFirst方法,pollFirst方法实现了队列头部弹出。

小结


我们在本文中分析了ArrayDeque的实现、使用、及原理,下面来简单总结一下:

  1. ArrayDeque是一个实现了Deque接口的双向队列。
  2. ArrayDeque的容量大小是可以动态调整的,并且容量大小必须满足是2的n次幂。
  3. ArrayDeque内部是使用数组来实现数据存储的。
  4. ArrayDeque不是线程安全的。
  5. ArrayDeque禁止使用空元素。
  6. ArrayDeque用作堆栈时,比Stack要快,当它用作队列时,比LinkedList要快。
  7. ArrayDeque最大容量是2^30个元素。
  8. ArrayDeque有两个类属性,head和tail,两个指针,可以在数组任意一端进行数据插入或删除。
  9. ArrayDeque在Android中有大量的应用。
  10. 在队列头部添加元素,可使用addFirst方法,在队列尾添加元素可使用addLast方法,队列头弹出元素可使用pollFirst方法,队列尾弹出元素可使用pollLast方法。
  11. ArrayDeque的offer方法可以实现在队尾添加元素,它内部是调用offerLast方法实现的。
  12. ArrayDeque的poll方法可以在队列头弹出元素,它的内部是调用pollFirst方法实现的。
  13. ArrayDeque中,大量使用了位操作,非常巧妙的省去了边界处理等问题,简化了操作。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/191250.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 什么是语义分割_词法分析语法分析语义分析

    什么是语义分割_词法分析语法分析语义分析文章目录引言1混淆矩阵2语义分割PA:像素准确率CPA:类别像素准确率MPA:类别平均像素准确率IoU:交并比MIoU:平均交并比(改进,先求IoU,再求MIoU,这里有误)3综合实例步骤一:输入真实、预测图片步骤二:求出混淆矩阵步骤三:评价指标计算PACPAMPAIoUMIoU4测试代码参考引言语义分割是像素级别的分类,其常用评价指标:像素准确率(PixelAccuracy,PA…

  • 中国大推力矢量发动机WS15 跨入 世界先进水平!

    中国大推力矢量发动机WS15 跨入 世界先进水平!

  • PLSQL 使用教程

    PLSQL 使用教程1打开表打开PLSQL点击Objects在下拉列表中选中Myobjects找到Table下级目录中都是数据库的表2查看表的数据选中表,右击点击-查看数据/querydata弹出sql窗口,可以查看数据3查看表结构选中表,右击点击-查看/View…

  • php三个数从大到小排列_php常用的流程控制语句

    php三个数从大到小排列_php常用的流程控制语句<?php$a = rand(100,999);$b = rand(100,999);$c = rand(100,999);echo “a=”.”$a”.”<br>”;echo “b=”.”$b”.”<br>”;echo “c=”.”$c”.”<br>”;if(($a > $b ) && ($a > …

  • restful接口定义_主板上的spi接口接什么

    restful接口定义_主板上的spi接口接什么由于在实际项目中碰到的restful服务,参数都以json为准。这里我获取的接口和传入的参数都是json字符串类型。发布restful服务可参照文章http://www.cnblogs.com/jav

  • 项目POC_poc技术

    项目POC_poc技术PoC(ProofofConcept),即概念验证。通常是企业进行产品选型时或开展外部实施项目前,进行的一种产品或供应商能力验证工作。验证内容1、产品的功能。产品功能由企业提供,企业可以根据自己的需求提供功能清单,也可以通过与多家供应商交流后,列出自己所需要的功能;2、产品的性能。性能指标也是由企业提供,并建议提供具体性能指标所应用的环境及硬件设备等测试环境要求;3、产品的API适用性;4、产…

    2022年10月28日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号