ArrayDeque in-depth analysis

Work together to create and grow together! This is the 31st day of my participation in the "Nuggets Daily New Plan · August Update Challenge", click to view the details of the event

Overview

Don't you know that ArrayDeque is a container that you use a lot in your daily work? It is a very powerful container. It can be used as a queue to realize the function of FIFO first in, first out, and also has the function of stack to realize the function of FILO first in last out. So what is it like? What about performance?

ArrayDeque introduction

ArrayDeque is mainly a Deque (Double Ended Queue) double-ended queue based on an array, that is, a double-ended queue, which can be used as a stack or a queue.

  • ArrayDeque is a double-ended queue with no capacity limit. The bottom layer is implemented based on arrays and will automatically expand.
  • ArrayDeque is not thread safe.
  • ArrayDeque cannot access null elements.
  • When used as a stack, the performance is better than Stack; when used as a queue, the performance is better than LinkedList.

The above is the class structure diagram of ArrayDeque:

  • Implements the Queue interface, which is actually a single-ended queue and can only operate on one end of the queue.
  • The Deque interface is implemented, Deque integrates the Queue interface, and there are apis that can operate both ends of the queue at the same time.
  • Implements the Cloneable interface, indicating that the queue supports clone.
  • Implements the Serializable interface, marking the interface to support serialization operations.

Construction method

method illustrate
ArrayDeque() Constructs an array deque with an initial capacity of 16
ArrayDeque(int numElements) Constructs an array deque with an initial capacity of numElements
ArrayDeque(Collection<? extends E> c) Constructs an array deque with initial contents not c

key method

Add related methods

method Equivalent method illustrate
add(e) addLast(e) add an element to the end of the queue
offer(e) offer load(s) add an element to the end of the queue
addFirst(e) offerFirst Add an element to the head of the queue
  • The method of the add prefix, if the capacity limit is exceeded, the addition fails, and a runtime exception will be thrown
  • 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的这种情况,非常的巧妙。

扩容操作:

After each element is added, if the head index and the tail index meet, it means that the array space is full and needs to be expanded. Each expansion of ArrayDeque will double the original capacity, which is also a guarantee that the capacity must be a power of 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() takes elements
 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 points to the next empty position, so it (tail - 1) & (elements.length - 1)is equivalent to calculating the previous index position, getting the value in it, and then setting the element to be empty.

Summarize

ArrayDeque is a concrete implementation of the Deque interface, which relies on mutable arrays. ArrayDeque has no capacity limit and can automatically expand according to demand. ArrayDeque can be used as a stack, which is more efficient than Stack; both ArrayDeque and LinkedList also implement the Deque interface, which one should be used? If you only need the Deque interface and operate from both ends, generally speaking, ArrayDeque is more efficient and should be used first. However, if you need to operate according to the index position at the same time, or often need to insert and delete in the middle, you should Select LinkedList.

refer to

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

www.cnblogs.com/carpenterle…

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

Guess you like

Origin juejin.im/post/7136915645370728479