ArrayDeque解析

Posted yweihainan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayDeque解析相关的知识,希望对你有一定的参考价值。

Queue接口

1 public abstract boolean add(E paramE);
2 public abstract boolean offer(E paramE);  // 加入元素
3 public abstract E remove();
4 public abstract E poll(); // 获取元素

Queue接口提供了以上几个方法。

Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。

它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。

如果要使用前端而不移出该元素,使用element()或者peek()方法。

 

ArrayDeque

ArrayDeque的特点

  • 大小自增长的队列
  • 内部使用数组存储数据
  • 线程不安全
  • 内部数组长度为8、16、32….. 2的n次方
  • 头指针head从内部数组的末尾开始,尾指针tail从0开始,在头部插入数据时,head减一,在尾部插入数据时,tail加一。当head==tail时说明数组的容量满足不了当前的情况,此时需要扩大容量为原来的二倍。

核心思想图

技术分享

 

 

代码解析

  • 分配数组大小
 1 private void allocateElements(int numElements) {
 2         int initialCapacity = MIN_INITIAL_CAPACITY;
 3         // 如果指定的数组长度大于最小的值,需要计算数组大小。
 4         // 算法利用或运算和右移运算,计算结果始终为2的n次方,比如numElements的值为4、5、6、7时,initialCapacity的结果为8;numElements的值为8、9、10、11、12、13、14、15时,initialCapacity的结果为16。
 5         // 设计数组的长度为2的n次方是为循环链表考虑的,后面会解释。
 6         if (numElements >= initialCapacity) {
 7             initialCapacity = numElements;
 8             initialCapacity |= (initialCapacity >>>  1);
 9             initialCapacity |= (initialCapacity >>>  2);
10             initialCapacity |= (initialCapacity >>>  4);
11             initialCapacity |= (initialCapacity >>>  8);
12             initialCapacity |= (initialCapacity >>> 16);
13             initialCapacity++;
14             // 如果值太大溢出(即值小于0),需要缩小2倍
15             if (initialCapacity < 0)   // Too many elements, must back off
16                 initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
17         }
18         elements = new Object[initialCapacity];
19     }
  • 扩大数组长度
 1 private void doubleCapacity() {
 2         // assert head == tail;
 3         int p = head;
 4         int n = elements.length;
 5          // 头指针右边的长度
 6         int r = n - p;
 7         // 扩大2倍
 8         int newCapacity = n << 1;
 9         // 检测新的数组长度是否太大
10         if (newCapacity < 0)
11             throw new IllegalStateException("Sorry, deque too big");
12         Object[] a = new Object[newCapacity];
13         // 复制头指针右边的数据
14         System.arraycopy(elements, p, a, 0, r);
15         // 复制头指针左边的数据
16         System.arraycopy(elements, 0, a, r, p);
17         elements = a;
18         // 初始化头指针为0
19         head = 0;
20         // 初始化尾指针为n
21         tail = n;
22     }
  • 在头部插入数据
 1 public void addFirst(E e) {
 2         if (e == null)
 3             throw new NullPointerException("e == null");
 4         // 在头部插入数据,头指针向左移动1,即减一,elements.length为2的n次方,所以elements.lenght-1的二进制表示为1111....1111。
 5         // 特殊情况:假设数组的长度为16,初始化时head为0,那么(head - 1) & (elements.length - 1)就是-1 & 15的值,负数的二进制为对应正数的二进制的补码,-1即为11111....1111,那么-1 & 15 = 15,如上图,当首次向队列头部插入数据时,head在数组的末尾。
 6         elements[head = (head - 1) & (elements.length - 1)] = e;
 7         // 头指针和尾指针相等时(即相遇),表示当前的数组长度不够,需要扩大数组长度
 8         if (head == tail)
 9             doubleCapacity();
10     }
  • 在尾部插入数据
1 public void addLast(E e) {
2         if (e == null)
3             throw new NullPointerException("e == null");
4         elements[tail] = e;
5         // tail向右移动,如果头指针和尾指针相等时(即相遇),表示当前的数组长度不够,需要扩大数组长度
6         if ( (tail = (tail + 1) & (elements.length - 1)) == head)
7             doubleCapacity();
8     }
  • 遍历
 1 private class DeqIterator implements Iterator<E> {
 2         // 从头指针位置开始遍历
 3         private int cursor = head;
 4         private int fence = tail;
 5         private int lastRet = -1;
 6 
 7         public boolean hasNext() {
 8             return cursor != fence;
 9         }
10 
11         public E next() {
12             if (cursor == fence)
13                 throw new NoSuchElementException();
14             @SuppressWarnings("unchecked") E result = (E) elements[cursor];
15 
16             if (tail != fence || result == null)
17                 throw new ConcurrentModificationException();
18             lastRet = cursor;
19             // 每次加一,利用与运算,形成循环。比如,数组的长度为16,当cursor递增到15时,下面的计算应为16 & 15 = 0,cursor的值变为了0.
20             cursor = (cursor + 1) & (elements.length - 1);
21             return result;
22         }
23 
24         public void remove() {
25             if (lastRet < 0)
26                 throw new IllegalStateException();
27             if (delete(lastRet)) { // if left-shifted, undo increment in next()
28                 cursor = (cursor - 1) & (elements.length - 1);
29                 fence = tail;
30             }
31             lastRet = -1;
32         }
33     }

 

 

有以下几点总结:

1)ArrayDeque有两个类属性,head和tail,两个指针。

2)ArrayDeque通过一个数组作为载体,其中的数组元素在add等方法执行时不移动,发生变化的只是head和tail指针,而且指针是循环变化,数组容量不限制。

3)offer方法和add方法都是通过其中的addLast方法实现,每添加一个元素,就把元素加到数组的尾部,此时,head指针没有变化,而tail指针加一,因为指针是循环加的,

     所以当tail追上head((this.tail = this.tail + 1 & this.elements.length - 1) == this.head)时,数组容量翻一倍,继续执行。

4)remove方法和poll方法都是通过其中的pollFirst方法实现,每移除一个元素,该元素所在位置变成null,此时,tail指针没有变化,而head指针加一,当数组中没有数据时,返回null。

5)因为ArrayDeque不是线程安全的,所以,用作堆栈时快于 Stack,在用作队列时快于 LinkedList。

以上是关于ArrayDeque解析的主要内容,如果未能解决你的问题,请参考以下文章

Java 集合深入理解 (十三) :ArrayDeque实现原理研究,及动态扩容双端队列和单队列和栈比较

ArrayDeque 源码分析

片段(Java) | 机试题+算法思路+考点+代码解析 2023

死磕 java集合之ArrayDeque源码分析

无法解析片段中的 findViewById [重复]

JDK源码ArrayDeque源码分析