ArrayDeque是java中对双端队列的线性实现
一.特性
- 无容量大小限制,容量按需增长;
- 非线程安全队列,无同步策略,不支持多线程安全访问;
- 当用作栈时,性能优于Stack,当用于队列时,性能优于LinkedList;
- 两端都可以操作;
- 具有fail-fast特征;
- 不能存储null;
- 支持双向迭代器遍历
ArrayDeque的迭代器和大多数容器迭代器一样,都是快速失败(fail-fast),但是程序不能利用这个特性决定是或否进行了并发操作。
二.实现和继承
ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
实现的接口
Cloneable、Serializable无接口,Deque接口如下,大致看看,不做详细了解
public interface Deque<E> extends Queue<E>{
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
boolean removeFirstOccurrence(Object o);
boolean removeLastOccurrence(Object o);
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
void push(E e);
E pop();
boolean remove(Object o);
boolean contains(Object o);
public int size();
Iterator<E> iterator();
Iterator<E> descendingIterator();
}
继承AbstractCollection
public abstract class AbstractCollection<E> implements Collection<E>{
protected AbstractCollection() {}
public abstract Iterator<E> iterator();
public abstract int size();
public boolean isEmpty() {
return size() == 0;
}
public boolean contains(Object o) {
Iterator<E> it = iterator();
...
}
public Object[] toArray() {
Object[] r = new Object[size()];
...
}
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
...
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
@SuppressWarnings("unchecked")
private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
int i = r.length;
...
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError("Required array size too large");
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public boolean remove(Object o) {
Iterator<E> it = iterator();
...
}
public boolean containsAll(Collection<?> c) {
for (Object e : c)
if (!contains(e))
return false;
return true;
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public void clear() {
Iterator<E> it = iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
}
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
}
三.源码分析
1.ArrayDeque数据域
/**
* 用于存储deque元素的数组。
* deque的容量是这个数组的长度总是二次方。永远不允许数组变成已满,
* 除非在addX方法中临时存在在变满后立即调整大小,从而避免了头尾缠绕相等其他。
* 我们还保证所有阵列单元不保持deque元素始终为null
*/
transient Object[] elements; // 非私有以简化嵌套类访问
/**
* 元素在deque头部的索引(这是将通过remove()或pop()移除的元素);
* 或者如果deque为空则为等于tail的任意数字。
*/
transient int head;
/**
* 下一个元素将被添加到deque尾部的索引(通过addLast(E)、add(E)或push(E))。
*/
transient int tail;
/**
* 我们将用于新创建的deque的最小容量。必须是2的幂。
*/
private static final int MIN_INITIAL_CAPACITY = 8;
首先看下ArrayDeque持有的成员域,其中非常核心的是elements,head,tail三个。
- elements:
该数组用于存储队列元素,且是大小总是2的幂次方(后面会介绍为什么?)。这个数组不会满容量,会在add方法中扩容,使得头head和tail不会缠绕在一起(即head增长或不会超过tail,head减小时不会溢出到tail),这里队列长度是2的幂次方的原因后续会阐明; - head: 双端队列的头位置,出队时或者弹出栈时的元素位置,加入双端队列头端元素位置,表示当前头元素位置;
- tail:双端队列的尾,入队和进栈时的元素位置,加入双端队列尾端的下个元素的索引,tail位总是空的;
- MIN_INITIAL_CAPACITY:最小的初始化容量
2.构造函数
/**
* 构造一个具有初始容量的空数组deque足以容纳16个元素。
*/
public ArrayDeque() {
elements = new Object[16];
}
/**
* 构造一个具有初始容量的空数组deque足以容纳指定数量的元素。
*@param numElements关于deque初始容量的下限
*/
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
/**
* 构造一个包含指定的元素的deque集合,按集合的返回顺序迭代器。
* @param c 要将其元素放入deque中的集合如果指定的集合为null,
* 则@throws NullPointerException
*/
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
- 第一个默认的无参构造函数:创建初始化大小为16的队列
- 第二个构造函数:根据参数numElements创建队列,如果numElements小于8,则队列初始化大小为8;如果numElements大于8,则初始化大小为大于numElements的最小2的幂次方。如:numElements=17,则初始化大小为32
- 第三个构造函数:根据集合元素创建队列,初始化大小为大于集合大小的最小2的幂次方。
3.ArrayDeque(Collection<? extends E> c)构造函数
这里重点看下第二个构造器的过程。其中调用allocateElements(numElements)方法,该方法用来实现容量分配,下面看下内部具体实现:
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
/**
*分配空数组以容纳给定数量的元素
*/
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
/**
* 首先判断指定大小numElements与MIN_INITIAL_CAPACITY的大小关系。
* 如果小于MIN_INITIAL_CAPACITY,则直接分配大小为MIN_INITIAL_CAPACITY的数组;
* 如果大于MIN_INITIAL_CAPACITY,则进行无符号右移操作,然后在加1,
* 这样就可以寻找到大于numElements的最小2的幂次方。
*/
private static int calculateSize(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// 找到两个元素的最佳幂
// 测试“<=”,因为数组未保持满
if (numElements >= initialCapacity) {
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // 元素太多,必须后退
initialCapacity >>>= 1;// 好运分配2^30个元素
}
return initialCapacity;
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
首先判断指定大小numElements与MIN_INITIAL_CAPACITY的大小关系。如果小于MIN_INITIAL_CAPACITY,则直接分配大小为MIN_INITIAL_CAPACITY的数组;如果大于MIN_INITIAL_CAPACITY,则进行无符号右移操作,然后在加1,这样就可以寻找到大于numElements的最小2的幂次方。
原理:无符号右移再进行按位或操作,就是将其低位全部补成1,然后再自加加一次,就是再向前进一位。这样就能得到其最小的2次幂。之所以需要最多移16位,是为了能够处理大于2^16次方数。
最后再判断值是否小于0,因为如果初始值在int最大值231-1和230之间,进行一系列移位操作后将得到int最大值,再加1,则溢出变成负数,所以需要检测临界值,然后再右移1位!!!
小知识:>> 表示右移,如果该数为正,则高位补0,若为负数,则高位补1 ; >>> 表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0;
/**
* 小知识解析 负数在计算机中以补码表示,如果对char、byte、short类型移位,
* 则在移位前,它们会被转成int类型,且返回值也是int类型;如果对long类型移位,则返回值也是long。
*/
// -15原码 10000000 00000000 00000000 00001111
// 反码 11111111 11111111 11111111 11110000
// 补码 11111111 11111111 11111111 11110001
int bin_0b =0b11111111111111111111111111110001;
System.err.println(bin_0b);//-15
int aa = -15;// 11111111 11111111 11111111 11110001
//无符号右移两位 00111111 11111111 11111111 11111100
System.err.println(aa >>> 2); // 1073741820
//右移两位 高位补1 11111111 11111111 11111111 1111100
System.err.println(aa >> 2); // -4
System.err.println(Integer.toBinaryString(-4)); //11111111 11111111 11111111 11111100
// -4 原码 10000000 00000000 00000000 00000100
// 反码 11111111 11111111 11111111 11111011
// 补码 11111111 11111111 11111111 11111100
以50和1200000000计算示例如下
// 50
// 11001
// 110010
// 111011
System.err.println("位或第一次:" + 0b111011);
// 1110
// 111011
// 111111
System.err.println("位或第二次:" + 0b111111);
// 11
// 111111
// 111111
System.err.println("位或第三次:" + 0b111111);
// 111111
System.err.println("位或第四次:" + 0b111111);
// 111111
System.err.println("位或第五次:" + 0b111111);
// 111111
// 1000000
System.err.println("加1:" + 0b1000000); //64
System.err.println(calculateSize(50)); //64
// 1200000000
// 100011110000110100011000000000
// 1000111100001101000110000000000
// 1100111110001111100111000000000
System.err.println("位或第一次:" + 0b1100111110001111100111000000000);
// 11001111100011111001110000000
// 1100111110001111100111000000000
// 1111111111101111111111110000000
System.err.println("位或第二次:" + 0b1111111111101111111111110000000);
// 111111111110111111111111000
// 1111111111101111111111110000000
// 1111111111111111111111111111000
System.err.println("位或第三次:" + 0b1111111111111111111111111111000);
// 11111111111111111111111
// 1111111111111111111111111111000
// 1111111111111111111111111111111
System.err.println("位或第四次:" + 0b1111111111111111111111111111111);
// 111111111111111
// 1111111111111111111111111111111
System.err.println("位或第五次:" + 0b1111111111111111111111111111111);
// 1111111111111111111111111111111
// 10000000000000000000000000000000
System.err.println("加1:" + 0b10000000000000000000000000000000);
4.重要行为
addFirst方法
/**
* 在此deque的前面插入指定的元素.
* @param e 要添加的元素
* 如果指定的元素为null,则引发NullPointerException
*/
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
首先判断插入元素是否为空,再计算即将插入的位置,计算出后将元素赋值给相应的槽位,最后再判断队列容量进行扩容。
- 将数组的高位端作为双端队列的头部,将低位作为双端队列尾部。每从头部加入一个元素时,head头逆时针向tail尾方向移动一个位置,实现上即将head减1后对数组的最大下标按位与运算。这里就利用了2的幂次方的特性,队列容量设置为2的幂次方后,数组的最大下标位置等于2的幂次方减1,在二进制表示时,就是所有二进制位都是1。这样head位置减1后与其进行按位与运算就能得到头部插入的位置。
- 当head等于tail时,就表示队列已经满了。这时需要进行扩容。
再来看下扩容策略:
/**
*使这个deque的容量加倍. 仅在满时呼叫, 当头部和尾部缠绕在一起变得相等时
*/
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
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;
}
- 按照2倍方式扩容
- 扩容后,将原队列中从头部插入的元素即head右边元素从扩容后新数组的0位置开始排放,然后将左边的元素紧接着排放进新数组。
- 将head置0,tail置成扩容前数组长度。
addLast方法
/**
* 在该deque的末尾插入指定的元素.
* 此方法等效于{@link#add}
* @param e 添加的元素
* @throws 如果指定的元素为null,则引发NullPointerException
*/
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
上述的addFirst是逆时针的插入方式,addLast刚好与其相反,即顺时针方向插入,且tail表示的是下一个插入的元素的位置。
- 判断元素是否为空,然后直接将元素插入tail槽位
- 然后tail向后移动一位,再按位与(控制循环)作为新的tail槽位
- 判断新的tail槽位是否与head相等,然后依此进行扩容(这里扩容与上述扩容过程一样,不再赘述)。
pollFirst方法
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// Element is null if deque empty
if (result == null)
return null;
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1);
return result;
}
- 取出头元素,如果头元素为空,则返回null
- 否则,将头元素槽位置为空(因为pollFirst是移除操作)
- 再将head顺时针向后移动一位,即加1再和数组最大下标按位与计算出新的head
读到这里,相信读者已经已经对双端队列的数据结构已经非常清晰,即双端操作的数组,tail向前(顺时针)移动即从尾端插入元素或者向后移动即从尾端移除元素,head向后(逆时针)移动即从头端插入元素或者向前移动即从头端移除元素。这几个过程正好具有FIFO和LIFO的特点,所以ArrayDeque既可以作为队列Queue又可以作为栈Stack。
pollLast方法
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
E result = (E) elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
从以上描述的ArrayDeque的数据结构和tail的含义中,可以大致思考下,从尾端移除元素的过程。
- 先将tail向后(逆时针)移动一位,然后对数组最大下标按位与计算出将要移除元素的槽位
- 取出计算出的槽位中元素,判断是否为空,为空则返回null
- 如果不为空,则将该槽位置为空,将槽位下标作为新的tail
双向队列操作
插入元素
addFirst(): 向队头插入元素,如果元素为空,则发生NPE
addLast(): 向队尾插入元素,如果为空,则发生NPE
offerFirst(): 向队头插入元素,如果插入成功返回true,否则返回false
offerLast(): 向队尾插入元素,如果插入成功返回true,否则返回false
移除元素
removeFirst(): 返回并移除队头元素,如果该元素是null,则发生NoSuchElementException
removeLast(): 返回并移除队尾元素,如果该元素是null,则发生NoSuchElementException
pollFirst(): 返回并移除队头元素,如果队列无元素,则返回null
pollLast(): 返回并移除队尾元素,如果队列无元素,则返回null
获取元素
getFirst(): 获取队头元素但不移除,如果队列无元素,则发生NoSuchElementException
getLast(): 获取队尾元素但不移除,如果队列无元素,则发生NoSuchElementException
peekFirst(): 获取队头元素但不移除,如果队列无元素,则返回null
peekLast(): 获取队尾元素但不移除,如果队列无元素,则返回null
栈操作
pop(): 弹出栈中元素,也就是返回并移除队头元素,等价于removeFirst(),如果队列无元素,则发生NoSuchElementException
push(): 向栈中压入元素,也就是向队头增加元素,等价于addFirst(),如果元素为null,则发生NPE,如果栈空间受到限制,则发生IllegalStateException