ArrayDeque の詳細な分析

一緒に創造し、成長するために一緒に働きましょう!「ナゲッツデイリー新プラン・8月アップデートチャレンジ」参加31日目、イベント詳細はこちら

概要

ArrayDeque は、日常業務でよく使用するコンテナーであることをご存知ですか? FIFO 先入れ先出しの機能を実現するキューとしての機能と、FILO 先入れ後出しの機能を実現するスタックの機能を兼ね備えた非常に強力なコンテナです。パフォーマンスはどうですか?

ArrayDeque の紹介

ArrayDeque は、主に配列に基づく Deque (Double Ended Queue) 両端キュー、つまり、スタックまたはキューとして使用できる両端キューです。

  • ArrayDeque は、容量制限のない両端キューです. 最下層は配列に基づいて実装され、自動的に拡張されます.
  • ArrayDeque はスレッドセーフではありません。
  • ArrayDeque は null 要素にアクセスできません。
  • スタックとして使用する場合は Stack よりもパフォーマンスが高く、キューとして使用する場合は LinkedList よりもパフォーマンスが高くなります。

上記は ArrayDeque のクラス構造図です。

  • Queue インターフェイスを実装します。これは実際にはシングルエンド キューであり、キューの一方の端でのみ動作します。
  • Deque インターフェースが実装され、Deque は Queue インターフェースを統合し、キューの両端を同時に操作できる API があります。
  • Cloneable インターフェースを実装し、キューがクローンをサポートしていることを示します。
  • Serializable インターフェイスを実装し、シリアル化操作をサポートするインターフェイスをマークします。

施工方法

方法 例証する
ArrayDeque() 初期容量が 16 の配列両端キューを構築します
ArrayDeque(int numElements) 初期容量が numElements の配列両端キューを構築します
ArrayDeque(Collection<? extends E> c) c 以外の初期内容で配列両端キューを構築します

キー方式

関連するメソッドを追加

方法 同等の方法 例証する
追加(e) addLast(e) 要素をキューの最後に追加します
オファー(e) 負荷を提供する 要素をキューの最後に追加します
addFirst(e) オファーファースト キューの先頭に要素を追加します
  • プレフィックス メソッドを追加します。容量制限を超えた場合、追加は失敗し、実行時例外がスローされます
  • 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的这种情况,非常的巧妙。

扩容操作:

各要素が追加された後、ヘッド インデックスとテール インデックスが一致する場合は、配列スペースがいっぱいであり、拡張する必要があることを意味します。ArrayDeque を拡張するたびに、元の容量が 2 倍になります。これは、容量が 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() は要素を取ります
 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 は次の空の位置を指しているため(tail - 1) & (elements.length - 1)、前のインデックス位置を計算し、その値を取得してから、要素を空に設定することと同じです。

要約する

ArrayDeque は、変更可能な配列に依存する Deque インターフェースの具体的な実装です。ArrayDeque には容量制限がなく、必要に応じて自動的に拡張できます。ArrayDeque はスタックとして使用でき、これは Stack よりも効率的です。ArrayDeque と LinkedList も Deque インターフェイスを実装していますが、どちらを使用する必要がありますか? Deque インターフェースのみが必要で、両端から操作する場合、一般的に言えば、ArrayDeque の方が効率的であり、最初に使用する必要がありますが、同時にインデックス位置に従って操作する必要がある場合、または頻繁に挿入と削除が必要な場合途中で、LinkedList を選択する必要があります。

参照する

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

www.cnblogs.com/carpenterle…

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

おすすめ

転載: juejin.im/post/7136915645370728479