我们前面说过Deque接口,它是一个双端队列。除了有LinkedList这个实现类以外,它还有ArrayDeque这个实现类,内部是使用循环数组来实现的。
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
1. 成员变量
transient Object[] elements; //内部数组
transient int head; //队列头部索引
transient int tail; //队列尾部元素后一个位置索引,elements[tail]永远为null
2. 构造器
//空参构造,会创建一个长度为17的数组
public ArrayDeque() {
elements = new Object[16 + 1];
}
/*
有参构造:
numElements小于1时,数组长度初始化为1;
numElements等于int最大值时,数组长度初始化为int最大值;
否则,数组长度初始化为 numElements + 1;
*/
public ArrayDeque(int numElements) {
elements =
new Object[(numElements < 1) ? 1 :
(numElements == Integer.MAX_VALUE) ? Integer.MAX_VALUE :
numElements + 1];
}
//通过传入的集合容器构造
public ArrayDeque(Collection<? extends E> c) {
this(c.size());
copyElements(c);
}
3. 成员方法
-
增
- 从尾部增加
public boolean add(E e) {
addLast(e);
return true;
}
这个方法主要是调用addLast()
方法
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);
}
addLast()
方法中,将元素添加到尾部之后,调用了inc()
方法,来操作tail
static final int inc(int i, int modulus) {
if (++i >= modulus) i = 0;
return i;
}
在这个方法里,我们看到,先是将tail
值+1,然后再去比较tail于数组长度的大小。当tail
不在数组末尾时,直接返回tail
,而当tail
已经到达末尾,将tail
重新置0再返回,实现循环效果。
inc()
方法返回后,addLast()
方法比较了head
和tail
是否相等,如果相等,说明队列已满,就调用grow()
方法,增加队列容量。
private void grow(int needed) {
final int oldCapacity = elements.length; //队列当前容量
int newCapacity;
//当前容量小于64时,增量为 当前容量 + 2 ,否则,增量为 当前容量一半
int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);
if (jump < needed
|| (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)
//如果计算出的增量小于所需的增量或者当前容量加上计算出的增量后超过了最大数组长度
//调用newCapacity()方法计算新容量
newCapacity = newCapacity(needed, jump);
//创建新容量大小的数组并将原数组复制到新数组中,然后让elements指向新数组
final Object[] es = elements = Arrays.copyOf(elements, newCapacity);
if (tail < head || (tail == head && es[head] != null)) {
//如果tail < head (addAll方法调用时可能会出现) 或者 队列已满
int newSpace = newCapacity - oldCapacity; //增量
//将head及后面位置的元素往后挪增量个长度
System.arraycopy(es, head,
es, head + newSpace,
oldCapacity - head);
//head指针往后挪增量个长度,中间的元素全部置空
for (int i = head, to = (head += newSpace); i < to; i++)
es[i] = null;
}
}
//计算新容量
private int newCapacity(int needed, int jump) {
final int oldCapacity = elements.length, minCapacity;
//minCpacity赋值为旧的容量加上所需增量
if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {
//minCpacity大于最大数组长度
if (minCapacity < 0)
//minCpacity超过int范围,抛出异常
throw new IllegalStateException("Sorry, deque too big");
//返回整型最大值
return Integer.MAX_VALUE;
}
if (needed > jump)
//所需增量大于计算增量,返回minCpacity,即使用所需增量
return minCapacity;
//否则,就使用计算增量
//当使用计算增量计算出的新容量大于最大数组长度时,返回最大数组长度
return (oldCapacity + jump - MAX_ARRAY_SIZE < 0)
? oldCapacity + jump
: MAX_ARRAY_SIZE;
}
以addAll()
过程中的tail < head
情况来说明扩容的过程(数组长度是我随便设的):
当队列已满时扩容过程也大致如此。要注意的是,队列已满的判断条件为tail == head && es[head] != null
。而队列为空(即isEmpty()
方法)判断条件却是tail == head
,这是否有所矛盾呢?其实并不矛盾,因为队列是否已满都是在内部判断的,一旦队满,head
和tail
的值会立即调整,也就是说,在外部看来,只有队列为空时才会出现两者相等的情况。
- 从头部添加
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);
}
从头部添加的方法与向尾部添加的方法类似,只不过从头部添加操作的是head
指针,每当head
小于0时,便将head
置于数组的末尾,以实现循环的效果。
-
删
- 从头部删除
public E remove() {
return removeFirst();
}
public E removeFirst() {
E e = pollFirst();
if (e == null)
throw new NoSuchElementException();
return e;
}
remove()
方法主要调用了removeFirst()
方法,而removeFirst()
方法则主要是调用了pollFirst()
方法
public E pollFirst() {
final Object[] es;
final int h;
//查找位于head处的元素
E e = elementAt(es = elements, h = head);
if (e != null) { //元素不为空
es[h] = null; //置空
head = inc(h, es.length); //头指针后移
}
return e;
}
- 从尾部删除
从尾部删除和从头部删除原理类似,就不赘述了。
-
查
- 查看头部元素
public E peek() {
return peekFirst();
}
public E peekFirst() {
return elementAt(elements, head);
}
static final <E> E elementAt(Object[] es, int i) {
return (E) es[i];
}
查看尾部元素也类似,原理都比较简单,就不分析了。
由于也是Deque的实现类,所以ArrayDeque也实现了栈和队列的方法,想把它当作栈和队列使用时,只需要用相应的接口去调用对应的方法就好了,上一篇LinkedList也已经说过了。
另外,由于ArrayDeque中并没有队列长度(即实际元素的个数)的字段,所以要通过计算得到size:
public int size() {
return sub(tail, head, elements.length);
}
static final int sub(int i, int j, int modulus) {
//tail小于head时,队列size等于数组长度+tail-head
//否则,队列size直接等于tail-head
if ((i -= j) < 0) i += modulus;
return i;
}
4. 特点
ArrayDeque实现了双端队列,内部使用循环数组实现,这决定了它有如下特点:
- 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加N个元素的效率为O(N)。
- 根据元素内容查找和删除的效率比较低,为O(N)。
- 与ArrayList和LinkedList不同,没有索引位置的概念,不能根据索引位置进行操作。
ArrayDeque和LinkedList都实现了Deque接口,应该用哪一个呢?如果只需要Deque接口,从两端进行操作,一般而言,ArrayDeque效率更高一些,应该被优先使用;如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选LinkedList。