剖析 ArrayDeque
剖析 ArrayDeque
Sun除了 LinkedList
,Java 容器类中还有一个双端队列的实现类
ArrayDeque
,它是基于数组实现的。一般而言,由于需要移动元素,数组的插入和删除效率比较低,但
ArrayDeque
的效率却非常高。
ArrayDeque
实现了 Deque
接口,同
LinkedList
一样,它的队列长度也是没有限制的,Deque
扩展了
Queue
,有队列的所有方法,还可以看作栈,栈的基本方法
push/pop/peek
,还有明确的操作两端的方法如
addFirst/removeLast
等。
ArrayDeque
内部有以下实例变量
1 | transient Object[] elements; |
elements
是存储元素的数组。ArrayDeque
的高效来源于 head
和 tail
这两个变量,它们使得物理上简单的从头到尾的数组变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。
- 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加
个元素的效率为 。 - 根据元素内容查找和删除的效率比较低,为
。 - 与
ArrayList
和LinkedList
不同,没有索引位置的概念,不能根据索引位置进行操作。
循环数组
循环数组是指元素到数组尾部之后可以接着从数组头开始,数组的长度、第一个和最后一个元素都与
head
和 tail
这两个变量有关:
- 如果
head
和tail
相同且数组为空,说明队列长度为 0 - 如果
tail
大于head
,则第一个元素为elements[head]
,最后一个元素为elements[tail-1]
,队列长度为tail-head
,元素索引从head
到tail-1
- 如果
tail
小于head
, 且为 0, 则第一个元素为elements[head]
, 最后一个为elements[elements.length-1]
, 元素索引从head
到elements.length-1
- 如果
tail
小于head
, 且大于 0, 则会形成循环, 第一个元素为elements[head]
, 最后一个是elements[tail-1]
, 元素索引从head
到elements.length-1
, 然后再从0
到tail-1
构造方法
ArrayDeque
有如下构造方法:
1 | public ArrayDeque() |
先看无参构造函数
1 | public ArrayDeque() { |
默认会创建一个长度为 16 的数组
如果有参数 numElements
,会调用
allocateElements
方法构造一个数组
1 | public ArrayDeque(int numElements) { |
allocateElements
方法会创建一个数组,数组的长度由
calculateSize
方法传入 numElements
参数得到,计算逻辑如下:
- 如果
numElements
小于 8, 就是 8。 - 在
numElements
大于等于 8 的情况下, 分配的实际长度是严格大于 numElements 并且为 2 的整数次幂的最小值。例如, 如果 numElements 为 10, 则实际分配 16, 如果 numElements 为 32, 则为 64。
1 | private void allocateElements(int numElements) { |
2
的幂次数会使得很多操作的效率很高,因为循环数组必须时刻至少留一个空位,
tail
变量指向下一个空位, 为了容纳 numElements
个元素, 至少需要 numElements + 1
个位置,所以要严格大于
numElements
最后一个构造方法:
1 | public ArrayDeque(Collection<? extends E> c) { |
同样调用 allocateElements
创建数组,随后调用了
addAll
方法
1 | public boolean addAll(Collection<? extends E> c) { |
addAll
方法只是循环调用了 add
方法
从尾部添加 add
add
方法用于向队列尾部添加元素,调用的是
addLast
方法
1 | public boolean add(E e) { |
addLast
方法将指定元素插入到此双端队列的末尾,将元素添加到 tail
处,然后将 tail
指向下一个位置
当 tail
到达数组的最后一个位置后,下一个元素应该被放入数组的起始位置,最直观的实现方式是将
tail
对数组长度取模,即
tail = (tail + 1) % elements.length;
一个数
数组长度为 2 的整数次幂,所以
tail = (tail + 1) & (elements.length - 1)
就能起到
tail
对数组长度取模的效果,并且性能更好
如果
e==null
会抛出NullPointerException
异常,说明ArrayDeque
不能存储null
值
1 | public void addLast(E e) { |
如果此时 tail == head
说明队列已经满了,要调用
doubleCapacity()
方法对数组进行扩容,doubleCapacity()
方法将数组扩容为原来的两倍
1 | private void doubleCapacity() { |
分配一个长度翻倍的新数组 a
,将 head
右边的元素复制到新数组开头处,再复制左边的元素到新数组中,最后重新设置
head
和 tail
,head
设为 tail
设为
从头部添加 addFirst
addFirst()
方法的代码为
1 | public void addFirst(E e) { |
在头部添加,要先让 head
指向前一个位置,然后再赋值给
head
所在位置。head的前一个位置是
(head-1) & (elements.length-1)
刚开始head为 0
,如果elements.length
为
8
,则(head-1) & (elements.length-1)
的结果为
7
。比如,执行如下代码:
1 | Deque<String> queue = new ArrayDeque<>(7); |
执行完成后内部结构如下图所示:
从头部删除 removeFirst
removeFirst()
方法的代码为
1 | public E removeFirst() { |
主要调用了 pollFirst()
方法,得到 result
后将原头部位置置为 null
,然后 head
置为下一个位置,即 (h + 1) & (elements.length - 1)
1 | public E pollFirst() { |
尾部删除元素的代码类似,只不过将 tail
置为前一个位置,即
(tail - 1) & (elements.length - 1)
查看长度 size
ArrayDeque
没有单独的字段维护长度,其 size
方法的代码为:
1 | public int size() { |
检查指定元素是否存在 contains
contains
方法的代码为:
1 | public boolean contains(Object o) { |
就是从 head
开始遍历并进行对比,循环过程中没有使用
tail
,而是到元素为 null
就结束了,这是因为在
ArrayDeque
中,有效元素不允许为 null
。