Análisis en profundidad de ArrayDeque

¡Trabajar juntos para crear y crecer juntos! Este es el día 31 de mi participación en el "Nuevo plan diario de Nuggets · Desafío de actualización de agosto", haga clic para ver los detalles del evento

Visión general

¿No sabes que ArrayDeque es un contenedor que usas mucho en tu trabajo diario? Es un contenedor muy potente. Se puede utilizar como cola para realizar la función de FIFO primero en entrar, primero en salir, y también tiene la función de pila para realizar la función de FILO primero en entrar, último en salir. Entonces, ¿cómo es? ¿Qué pasa con el rendimiento?

ArrayDeque introducción

ArrayDeque es principalmente una cola de doble extremo Deque (Double Ended Queue) basada en una matriz, es decir, una cola de doble extremo, que se puede usar como una pila o una cola.

  • ArrayDeque es una cola de dos extremos sin límite de capacidad. La capa inferior se implementa en función de los arreglos y se expandirá automáticamente.
  • ArrayDeque no es seguro para subprocesos.
  • ArrayDeque no puede acceder a elementos nulos.
  • Cuando se usa como pila, el rendimiento es mejor que Stack; cuando se usa como cola, el rendimiento es mejor que LinkedList.

Lo anterior es el diagrama de estructura de clases de ArrayDeque:

  • Implementa la interfaz Queue, que en realidad es una cola de un solo extremo y solo puede operar en un extremo de la cola.
  • Se implementa la interfaz Deque, Deque integra la interfaz Queue y hay API que pueden operar ambos extremos de la cola al mismo tiempo.
  • Implementa la interfaz Cloneable, lo que indica que la cola admite la clonación.
  • Implementa la interfaz Serializable, marcando la interfaz para admitir operaciones de serialización.

Método de construcción

método ilustrar
ArrayDeque() Construye una matriz deque con una capacidad inicial de 16
ArrayDeque(int numElements) Construye una matriz deque con una capacidad inicial de numElements
ArrayDeque(Colección<? extiende E> c) Construye una matriz deque con contenido inicial no c

método clave

Agregar métodos relacionados

método método equivalente ilustrar
añadir (e) añadirÚltimo(e) añadir un elemento al final de la cola
oferta oferta de carga(s) añadir un elemento al final de la cola
añadirprimero(e) ofertaPrimero Añadir un elemento a la cabeza de la cola
  • Agregue el método de prefijo, si se excede el límite de capacidad, la adición falla y se lanzará una excepción de tiempo de ejecución
  • offer前缀的方法,比较特殊,如果超过容量限制,会返回指定值true false

队列获取元素相关方法

等价方法 说明
remove() removeFirst() 获取并且删除队列头部元素
poll() pollFirst() 获取并且删除队列头部元素
removeLast() pollLast() 获取并且删除队列尾部
  • remove前缀的方法,如果容量为空,会抛出运行时异常
  • offer前缀的方法,如果容量为空,会返回指定值null

查看相关方法

等价方法 说明
element() getFirst() 查看队列头部元素
peek() peekFirst() 查看队列头部元素
getLast() peekLast() 查看队列尾部元素
  • peek前缀的方法,如果容量为空,会返回指定true,false,其他方法失败会抛出异常。

栈相关方法

等价方法 说明
push(e) addFirst(e) 向栈中添加元素
pop() removeFirst() 获取栈顶元素
peek() peekFirst() 查看栈顶元素

其他方法

方法 说明
removeFirstOccurrence(Object o) 删除队列中第一次相等的元素
removeLastOccurrence(Object o) 删除队列中最后一个相等的元素

tips:具体操作是返回指定值还是抛出异常,建议看源码的javadoc,写的非常清楚了。

使用案例

  1. 测试队列功能
  @Test
    public void test1() {
        Deque<String> deque = new ArrayDeque<>();
        deque.add("1");
        deque.offer("2");
        deque.offerLast("3");
        System.out.println(deque);
        String poll = deque.poll();
        System.out.println(poll);
        System.out.println(deque);
    }
复制代码

运行结果:

  1. 测试栈的功能
@Test
    public void test2() {
        Deque<String> deque = new ArrayDeque<>();
        deque.push("1");
        deque.push("2");
        deque.push("3");
        String pop = deque.pop();
        System.out.println(pop);
    }
复制代码

运行结果:

  1. 测试存储null数据
@Test
    public void test3() {
        Deque<String> deque = new ArrayDeque<>();
        boolean offerResult = deque.offer(null);
        System.out.println(offerResult);
        System.out.println(deque);
    }
复制代码

运行结果:

  1. 测试poll和remove的区别
@Test
    public void test4() {
        Deque<String> deque = new ArrayDeque<>();
        String poll = deque.poll();
        //取出为null
        System.out.println(poll);

        // 因为容量为空了,会抛出异常
        String remove = deque.remove();
        System.out.println(remove);
        System.out.println(deque);
    }
复制代码

运行结果:

核心机制

实现机制

从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组,也就是说数组的任何一点都可能被看作起点或者终点。

上图中我们看到,head指向首端第一个有效元素,tail指向尾端第一个可以插入元素的空位。因为是循环数组,所以head不一定总等于0,tail也不一定总是比head大。

总的来说,ArrayDeque内部它是一个动态扩展的循环数组,通过head和tail变量维护数组的开始和结尾,数组长度为2的幂次方,使用高效的位操作进行各种判断,以及对head和tail的维护。

源码解析

  • 构造放方法ArrayDeque(int numElements)
public ArrayDeque(int numElements) {
        // 初始化数组
        allocateElements(numElements);
    }

private void allocateElements(int numElements) {
        // 初始化数组,通过calculateSize方法计算数组长度
        elements = new Object[calculateSize(numElements)];
    }
复制代码
private static int calculateSize(int numElements) {
        // 设置初始容量等于8
        int initialCapacity = MIN_INITIAL_CAPACITY;
        //  如果numElement大于等于初始容量        	
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            initialCapacity |= (initialCapacity >>>  1);
            initialCapacity |= (initialCapacity >>>  2);
            initialCapacity |= (initialCapacity >>>  4);
            initialCapacity |= (initialCapacity >>>  8);
            initialCapacity |= (initialCapacity >>> 16);
            initialCapacity++;

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

是无符号右移操作,|是位或操作, 举个例子:

ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(10);
复制代码
  1. 程序运行到第五行时,numElements >= initialCapacity成立,10>=8,则会进入到if语句内部。
  2. 程序运行到第六行时, initialCapacity = numElements,initialCapacity 设置为10,10的二进制表示方式是1010。
  3. 程序运行到第七行时, initialCapacity无符号向右移动一位,1010无符号向右移动一位是0101,1010|0101=1111,十进制表示方式是15。
  4. 程序运行到第八行时, initialCapacity无符号向右移动2位,1111无符号向右移动一位是0011,1111|0011=1111,十进制表示方式是15,一直持续下去都是15,当程序运行到第12行时,15进行加1操作,则变成16。这个时候16就是2的幂次方返回。

整体思路是每次移动将位数最高的值变成1,从而将二进制所有位数都变成1,变成1之后得到的十进制加上1之后得到值就是2的幂次方的值。最终,数组长度永远都是是2的幂次方。

  • 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) & (elements.length - 1)使用的很巧妙,我们举个例子来讲解:

  ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(10);
    arrayDeque.addFirst(5);
复制代码

此时初始化时数组长度为16,头指针head和尾指针head默认是0,此时数组的内容如下所示:

现在来找执行addFirst操作后的head位置:

  • 初始head = 0, head -1 = -1, 对应的补码是11111111
  • elements.length 是16,elements.length - 1 是15,对应补码是00001111
  • 将上面两个数字&操作后得到结果00001111,正好是15

小结: 这段代码相当于取余,因为数组容量是2的幂次方,减去1的二进制位都是1,与1相与相当于它本身,同时也处理为为-1的这种情况,非常的巧妙。

扩容操作:

Después de agregar cada elemento, si el índice principal y el índice final se encuentran, significa que el espacio de la matriz está lleno y debe expandirse. Cada expansión de ArrayDeque duplicará la capacidad original, lo que también es una garantía de que la capacidad debe ser una potencia de 2.

private void doubleCapacity() {
        assert head == tail; //扩容时头部索引和尾部索引肯定相等
        int p = head;
        int n = elements.length;
        //头部索引到数组末端(length-1处)共有多少元素
        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;
    }
复制代码
  • pollLast() toma elementos
 public E pollLast() {
        //计算要取的元素索引
        int t = (tail - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        // 获取t位置的元素
        E result = (E) elements[t];
        if (result == null)
            return null;
        elements[t] = null;
        // 重新设置t
        tail = t;
        return result;
    }
复制代码

tail apunta a la siguiente posición vacía, por lo que (tail - 1) & (elements.length - 1)es equivalente a calcular la posición del índice anterior, obtener el valor en ella y luego configurar el elemento para que esté vacío.

Resumir

ArrayDeque es una implementación concreta de la interfaz Deque, que se basa en matrices mutables. ArrayDeque no tiene límite de capacidad y se puede ampliar automáticamente según la demanda. ArrayDeque se puede usar como una pila, que es más eficiente que Stack; ArrayDeque y LinkedList también implementan la interfaz Deque, ¿cuál se debe usar? Si solo necesita la interfaz Deque y opera desde ambos extremos, en términos generales, ArrayDeque es más eficiente y debe usarse primero. Sin embargo, si necesita operar de acuerdo con la posición del índice al mismo tiempo, o necesita insertar y eliminar con frecuencia en el medio, debe seleccionar LinkedList.

Referirse a

blog.jrwang.me/2016/java-c…

www.cnblogs.com/carpenterle…

www.cnblogs.com/swiftma/p/6…

Supongo que te gusta

Origin juejin.im/post/7136915645370728479
Recomendado
Clasificación