文章目录
- 剖析 ArrayDeque
- 3.1 循环数组
- 3.2 构造方法
- 3.3 从尾部添加 addLast(E)
- 3.4 从头部添加 addFirst(E)
- 3.5 从头部和尾部删除
- 3.6 查看长度 size()
- 3.7 检查给定元素是否存在
- 3.8 toArray
- 3.9 ArrayDeque 特点分析
剖析 ArrayDeque
本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
LinkedList
实现了队列接口Queue
和双端队列接口Deque
,Java
容器类中还有一个双端队列的实现类ArrayDeque
,它是基于数组实现的。我们知道,一般而言,由于需要移动元素,数组的插入和删除效率比较低,但ArrayDeque
的效率却非常高,它是怎么实现的呢?
ArrayDeque
有如下构造方法:
/**
* Constructs an empty array deque with an initial capacity
* sufficient to hold 16 elements.
*/
public ArrayDeque()
/**
* Constructs an empty array deque with an initial capacity
* sufficient to hold the specified number of elements.
*
* @param numElements lower bound on initial capacity of the deque
*/
public ArrayDeque(int numElements)
/**
* Constructs a deque containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator. (The first element returned by the collection's
* iterator becomes the first element, or <i>front</i> of the
* deque.)
*
* @param c the collection whose elements are to be placed into the deque
* @throws NullPointerException if the specified collection is null
*/
public ArrayDeque(Collection<? extends E> c)
ArrayDeque
实现了Deque
接口,同LinkedList
一样,它的队列长度也是没有限制的, Deque
扩展了Queue
,有队列的所有方法,还可以看作栈,有栈的基本方法push/pop/peek
,还有明确的操作两端的方法如addFirst/removeLast
等,具体用法与LinkedList
一节介绍的类似,重要的是它实现的原理。
ArrayDeque
内部主要有如下实例变量:
/**
* The array in which the elements of the deque are stored.
* All array cells not holding deque elements are always null.
* The array always has at least one null slot (at tail).
*/
transient Object[] elements;
/**
* 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 0 <= head < elements.length 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));
* elements[tail] is always null.
*/
transient int tail;
elements
就是存储元素的数组。ArrayDeque
的高效来源于head
和tail
这两个变量,它们使得物理上简单的从头到尾的数组变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。接下来,我们学习一下循环数组。
3.1 循环数组
对于一般数组,比如arr
,第一个元素为arr[0]
,最后一个为arr[arr.length-1]
。但对于ArrayDeque
中的数组,它是一个逻辑上的循环数组,所谓循环是指元素到数组尾之后可以接着从数组头开始,数组的长度、第一个和最后一个元素都与head
和tail
这两个变量有关,根据head
和tail
值之间的大小关系,可以具体分为四种情况:
- 如果
head
和tail
相同,则数组为空,长度为 0 0 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
。
上面情况,根据图示查看温习一遍。
3.2 构造方法
默认构造方法的代码为:
public ArrayDeque() {
elements = new Object[16 + 1];
}
分配了一个长度为
16
16
16的数组。如果有参数numElements
,代码为:
public ArrayDeque(int numElements) {
elements =
new Object[(numElements < 1) ? 1 :
(numElements == Integer.MAX_VALUE) ? Integer.MAX_VALUE :
numElements + 1];
}
看最后一个构造方法:
/**
* Constructs a deque containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator. (The first element returned by the collection's
* iterator becomes the first element, or <i>front</i> of the
* deque.)
*
* @param c the collection whose elements are to be placed into the deque
* @throws NullPointerException if the specified collection is null
*/
public ArrayDeque(Collection<? extends E> c) {
this(c.size());
copyElements(c);
}
先调用ArrayDeque(int numElements)
构造器,初始化elements
属性,再调用copyElements
函数,代码如下:
private void copyElements(Collection<? extends E> c) {
c.forEach(this::addLast);
}
遍历容器c
,调用addLast
函数。
3.3 从尾部添加 addLast(E)
addLast(E)
代码如下:
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
final Object[] es = elements;
es[tail] = e;
if (head == (tail = inc(tail, es.length)))
grow(1);
}
首先将元素添加到tail
处(es[tail] = e
),然后tail
指向下一个位置,调用inc(int i, int modulus)
函数确定tail
的下一位置,代码如下:
/**
* Circularly increments i, mod modulus.
* Precondition and postcondition: 0 <= i < modulus.
*/
static final int inc(int i, int modulus) {
if (++i >= modulus) i = 0;
return i;
}
tail
先加一,这时,分两种情况,如果tail
大于elements
的长度,那么tail
的值重置为
0
0
0,否则很简单,直接加一即可。
确认tail
后,通过head==tail
判断数组是否满员,如果满员,则调用grow
函数扩容,处理数组满员的情况,否则完成整个在尾部添加元素动作。grow
函数代码如下所示:
/**
* Increases the capacity of this deque by at least the given amount.
*
* @param needed the required minimum extra capacity; must be positive
*/
private void grow(int needed) {
// overflow-conscious code
final int oldCapacity = elements.length;
int newCapacity;
// Double capacity if small; else grow by 50%
int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);
if (jump < needed
|| (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
newCapacity = newCapacity(needed, jump);
final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
// Exceptionally, here tail == head needs to be disambiguated
if (tail < head || (tail == head && es[head] != null)) {
// wrap around; slide first leg forward to end of array
int newSpace = newCapacity - oldCapacity;
System.arraycopy(es, head,
es, head + newSpace,
oldCapacity - head);
for (int i = head, to = (head += newSpace); i < to; i++)
es[i] = null;
}
}
grow
传入一个needed
(扩张需求的数量)参数计算新容量(newCapacity
)。计算newCapacity
的过程中,先计算jump
值,计算公式:(oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)
,如果原来的长度低于
64
64
64,那么数组翻倍,否则增加
50
%
50\%
50%。得到jump
值后,如果需求大于供给,即:jump < needed
或者增加jump
后数组的长度大于允许的最大数组长度(MAX_ARRAY_SIZE
),那么调用newCapacity(int needed, int jump)
函数重新计算newCapacity
值,代码如下所示:
/** Capacity calculation for edge conditions, especially overflow. */
private int newCapacity(int needed, int jump) {
final int oldCapacity = elements.length, minCapacity;
if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {
if (minCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
return Integer.MAX_VALUE;
}
if (needed > jump)
return minCapacity;
return (oldCapacity + jump - MAX_ARRAY_SIZE < 0)
? oldCapacity + jump
: MAX_ARRAY_SIZE;
}
代码中,先计算允许的最小容量,计算公式:minCapacity = oldCapacity + needed
,即:原来的容量加上本次申请的容量。接下来需要分五种情况讨论:
minCapacity > Integer.MAX_VALUE
,即minCapacity
溢出,那么抛出IllegalStateException("Sorry, deque too big")
异常;MAX_ARRAY_SIZE < minCapacity <= Integer.MAX_VALUE
,返回Integer.MAX_VALUE
作为newCapacity
值;minCapacity<=MAX_ARRAY_SIZE and needed > jump
,返回minCapacity
作为newCapacity
值;minCapacity<=MAX_ARRAY_SIZE and needed <= jump and oldCapacity + jump<MAX_ARRAY_SIZE
,返回oldCapacity + jump
作为newCapacity
值;minCapacity<=MAX_ARRAY_SIZE and needed <= jump and oldCapacity + jump >= MAX_ARRAY_SIZE
,返回MAX_ARRAY_SIZE
作为newCapacity
值;
确认完newCapacity
后,分配一个newCapacity
长度的新数组es
,并将elementData
数组复制给es
,随后将head
右边的元素[head, oldCapacity)
,复制到[head+newSpace, newCapacity)
,再将[head, head+newSpace)
置为null
,最后更新head=head+newSpace
。
我们来看一个例子,例子如下图所示,添加元素
9
9
9:
3.4 从头部添加 addFirst(E)
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
final Object[] es = elements;
es[head = dec(head, es.length)] = e;
if (head == tail)
grow(1);
}
static final int dec(int i, int modulus) {
if (--i < 0) i = modulus - 1;
return i;
}
对比addLast(E)
代码,发现两者非常相像,一个是更新tail
值,一个是更新head
,更新完后,后面的代码完全一样,都是先判断head==tail
,如果成立,再调用grow(1)
,这里不重复分析。
3.5 从头部和尾部删除
从头部删除removeFirst
方法的代码为:
/**
* @throws NoSuchElementException {@inheritDoc}
*/
public E removeFirst() {
E e = pollFirst();
if (e == null)
throw new NoSuchElementException();
return e;
}
可知,removeFirst
调用的函数是pollFirst
,代码如下:
public E pollFirst() {
final Object[] es;
final int h;
E e = elementAt(es = elements, h = head);
if (e != null) {
es[h] = null;
head = inc(h, es.length);
}
return e;
}
代码比较简单,将原头部位置置为null
,然后head
置为下一个位置,下一个位置=原来head+1
,如果head+1>=es.length
,那么head=0
,否则head=head+1
。
从尾部删除removeLast()
调用了pollLast()
函数,代码如下:
/**
* @throws NoSuchElementException {@inheritDoc}
*/
public E removeLast() {
E e = pollLast();
if (e == null)
throw new NoSuchElementException();
return e;
}
public E pollLast() {
final Object[] es;
final int t;
E e = elementAt(es = elements, t = dec(tail, es.length));
if (e != null)
es[tail = t] = null;
return e;
}
static final int dec(int i, int modulus) {
if (--i < 0) i = modulus - 1;
return i;
}
代码比较简单,将原尾部位置置为null
,然后tail
置为上一个位置,上一个位置=原来的tail-1
,如果tail-1<0
,那么tail=es.length-1
,否则tail=tail-1
。
3.6 查看长度 size()
ArrayDeque
没有单独的字段维护长度,其size
方法的代码如下:
public int size() {
return sub(tail, head, elements.length);
}
static final int sub(int i, int j, int modulus) {
if ((i -= j) < 0) i += modulus;
return i;
}
通过该方法即可计算出size
。
3.7 检查给定元素是否存在
contains
方法的代码为:
public boolean contains(Object o) {
if (o != null) {
final Object[] es = elements;
for (int i = head, end = tail, to = (i <= end) ? end : es.length;
; i = 0, to = end) {
for (; i < to; i++)
if (o.equals(es[i]))
return true;
if (to == end) break;
}
}
return false;
}
如果head<=tail
,那么直接遍历[head,tail)
,判断给定元素是否存在;否则,遍历
[
h
e
a
d
,
e
s
.
l
e
n
g
t
h
)
⋃
[
0
,
t
a
i
l
)
[head,\ es.length)\ \bigcup\ [0,tail)
[head, es.length) ⋃ [0,tail),判断给定的元素是否存在。
3.8 toArray
toArray
方法toArray
方法的代码为:
public Object[] toArray() {
return toArray(Object[].class);
}
private <T> T[] toArray(Class<T[]> klazz) {
final Object[] es = elements;
final T[] a;
final int head = this.head, tail = this.tail, end;
if ((end = tail + ((head <= tail) ? 0 : es.length)) >= 0) {
// Uses null extension feature of copyOfRange
a = Arrays.copyOfRange(es, head, end, klazz);
} else {
// integer overflow!
a = Arrays.copyOfRange(es, 0, end - head, klazz);
System.arraycopy(es, head, a, 0, es.length - head);
}
if (end != tail)
System.arraycopy(es, 0, a, es.length - head, tail);
return a;
}
跟contains
一样,如果head<=tail
,那么直接复制[head,tail)
索引元素;否则,复制
[
h
e
a
d
,
e
s
.
l
e
n
g
t
h
)
⋃
[
0
,
t
a
i
l
)
[head,\ es.length)\ \bigcup\ [0,tail)
[head, es.length) ⋃ [0,tail)索引元素。
3.9 ArrayDeque 特点分析
ArrayDeque
内部维护一个动态扩展的循环数组,通过head
和tail
变量维护数组的开始和结尾。ArrayDeque
实现了双端队列,内部使用循环数组实现,这决定了它有如下特点。
- 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加 N N N个元素的效率为 O ( N ) O(N) O(N)。
- 根据元素内容查找和删除的效率比较低,为 O ( N ) O(N) O(N)。
- 与
ArrayList
和LinkedList
不同,没有索引位置的概念,不能根据索引位置进行操作。
ArrayDeque
和LinkedList
都实现了Deque
接口,应该用哪一个呢?如果只需要Deque
接口,从两端进行操作,一般而言,ArrayDeque
效率更高一些,应该被优先使用;如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选LinkedList
。
马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎
尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎