Java8 ArrayDeque 源码解析

作者: 没有颜色的菜 | 来源:发表于2018-09-12 23:36 被阅读13次

    前言

    今天我们来看看 ArrayDeque,可以说,之前使用的队列的场景不多,所以也没有深入研究队列,但最近在做 LeetCode 的时候,很多时候使用队列会有想不到的功效,比如我们在写 BFS 广度优先遍历的时候,或者在写 二叉树的层次遍历,非递归前序,中序,后序遍历,都会用到这个结构,如果还不会的同学,赶紧去复习一波吧,非常总要,也能为你的笔面试加不少分,很多时候当面试官问道我分析过的源码时,心里那个叫酸爽啊,令面试官刮目相看。闲话不多说了,让我们深入了解一下我们的主角

    前世今生

    实现了 Deque 接口,Deque 继承了 Queue

    public class ArrayDeque<E> extends AbstractCollection<E>
                               implements Deque<E>, Cloneable, Serializable {}
    
    public interface Deque<E> extends Queue<E> {}
    

    Queue

    在了解 ArrayDeque 之前,我们先来看看 Queue 有些什么东西,谈到队列,我们会想到先进先出等特性,我把接口贴出来给大家看一下,如果你还不熟悉这几个方法的区别,是该反省一下了!!!一定要熟记,这是基本功

        boolean add(E e);
    
        boolean offer(E e);
    
        E remove();
    
        E poll();
    
        E element();
    
        E peek();
    

    Deque

    由于是双端队列,多了很多接口,一张图真相,你可能会说,记住这些所有的太难了,太多了,但总结起来,也并不多,在之前的Queue的接口的基础上,加了 First,Last 的接口,是不是一下子变少了


    Screen Shot 2018-09-12 at 11.00.50 PM.png

    ArrayDeque

    终于到了我们的主角了,然而你可能会想,是不是也有LinkedDeque,当初我也这样想过,然而我并没有在 jdk 里找到这个数据结构,但是 LinkedList 却实现了 Deque 也就是说它取代了自己LinkedDeque,也就没有必要多此一举了

    属性

    既然是 Array,那么里面势必用数组存储,记住,使用 Object[] elements,而不是T[] elements,聪明的你能否想到这样做的目的呢?哈哈。然后是一个head,tail 的指针。

       transient Object[] elements; // non-private to simplify nested class access
    
        /**
         * The index of the element at the head of the deque (which is the
         * element that would be removed by remove() or pop()); or an
         * arbitrary number equal to tail if the deque is empty.
         */
        transient int head;
    
        /**
         * The index at which the next element would be added to the tail
         * of the deque (via addLast(E), add(E), or push(E)).
         */
        transient int tail;
    

    构造函数

    默认开辟了 16 的数组,一般都是这样,预先开辟空间,不够了再扩容。如果自己指定了大小,那么将调用 allocateElements函数,获取一个 2 的幂次方的大小的容量,如果你知道 Hashmap的初始化,那么这个初始化你一定不陌生!至于怎么做到,你可以自己实践一下,这样会理解的更加透彻!

        public ArrayDeque() {
            elements = new Object[16];
        }
        public ArrayDeque(int numElements) {
            allocateElements(numElements);
        }
     
        public ArrayDeque(Collection<? extends E> c) {
            allocateElements(c.size());
            addAll(c);
        }
    
        private void allocateElements(int numElements) {
            elements = new Object[calculateSize(numElements)];
        }
    
        private static int calculateSize(int numElements) {
            int initialCapacity = MIN_INITIAL_CAPACITY;
            // Find the best power of two to hold elements.
            // Tests "<=" because arrays aren't kept full.
            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;
        }
    

    普通的方法

    add 使用 addLast 在末尾追加元素

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

    addLast,直接给数组的末尾元素赋值,之后便是移动tail 指针,这里的扩容实现的很有意思,这也对应了为什么容量要为 2的幂次方,当数组大小刚好为 2 的幂次方时,(tail = (tail + 1) & (elements.length - 1) 为零,也就是说如果head也为0,那么就需要扩容了

        public void addLast(E e) {
            if (e == null)
                throw new NullPointerException();
            elements[tail] = e;
            if ( (tail = (tail + 1) & (elements.length - 1)) == head)
                doubleCapacity();
        }
    

    doubleCapacity 函数,新数组的大小为两倍,使用 System.arraycopy 函数复制,效率极高。因为 head 不一定为零,所以在扩容的时候,需要恢复head = 0;这里我们应该推测出整个数组都是存储数据,为了方便删除数组而不移动元素,便使用了指针记录状态,这一实现必须要好好琢磨,下次面试别人的时候可以问一下别人,哈哈,有点小坏

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

    pollFirst 获取元素,当然是从 head 位置获取元素,这里需要注意的是,head 如果到了数组末尾,那么又会通过 (h + 1) & (elements.length - 1) 变为 0 了

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

    pollLast 获取队尾元素,如果队尾元素此时为 0,那么将回到了 数组末尾,别问我怎么知道,老师叫你好好学二进制!

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

    其他的不涉及到删除,添加到操作就显得尤为简单了,直接获取,就不必多说了

        public E getFirst() {
            @SuppressWarnings("unchecked")
            E result = (E) elements[head];
            if (result == null)
                throw new NoSuchElementException();
            return result;
        }
    
        /**
         * @throws NoSuchElementException {@inheritDoc}
         */
        public E getLast() {
            @SuppressWarnings("unchecked")
            E result = (E) elements[(tail - 1) & (elements.length - 1)];
            if (result == null)
                throw new NoSuchElementException();
            return result;
        }
    

    pop 根据先进先出,pop 应该移除队首的元素,注意,这里会抛出异常,如果队首为空的话

         * @throws NoSuchElementException {@inheritDoc}
         */
        public E pop() {
            return removeFirst();
        }
    

    小结

    ArrayDeque 看起来不大,但也有不少东西,知道大概容易,弄懂每一个细节很难,如果你做到了,成功便是你的~

    相关文章

      网友评论

        本文标题:Java8 ArrayDeque 源码解析

        本文链接:https://www.haomeiwen.com/subject/sacigftx.html