1.ArrayDeque解析
1.1继承体系
由其名字可以看出,其是一个由数组
实现的双端队列,对比LinkedList是由链表
实现的双端队列,ArrayDeque实现了Queue接口,也是Collection下的一个常用类。
1.2常用方法
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);
// *** 队列中的方法 ***
// 添加元素,等于addLast(e)
boolean add(E e);
// 添加元素,等于offerLast(e)
boolean offer(E e);
// 移除元素,等于removeFirst()
E remove();
// 移除元素,等于pollFirst()
E poll();
// 查看元素,等于getFirst()
E element();
// 查看元素,等于peekFirst()
E peek();
// *** 栈方法 ***
// 入栈,等于addFirst(e)
void push(E e);
// 出栈,等于removeFirst()
E pop();
// *** Collection中的方法 ***
// 删除指定元素,等于removeFirstOccurrence(o)
boolean remove(Object o);
// 检查是否包含某个元素
boolean contains(Object o);
// 元素个数
public int size();
// 迭代器
Iterator<E> iterator();
// 反向迭代器
Iterator<E> descendingIterator();
}
2.核心结构
2.1属性
//底层的数组
transient Object[] elements;
//头指针
transient int head;
//尾指针
transient int tail;
//默认的最小容量,注意:elements的长度一定是2的次方幂。
private static final int MIN_INITIAL_CAPACITY = 8;
2.2构造器
- 调用无参构造器时,默认创建一个长度为
16
的数组。 - 调用传入初始容量n的构造器,当n小于8时,会初始化一个长度为8的一个数组。当n大于等于8时,会初始化一个长度为
<大于n的最小的2的幂>
的数组 (比如传入3算出来是8 传入9算出来是16 传入16算出来是32)
/* 1
* 空参构造器,底层初始化一个长度为16的数组
*/
public ArrayDeque() {
elements = new Object[16];
}
/* 2
* 传入初始容量,注意最终的容量是大于(没有等于)numElements的最大的2的幂
* 然后会创建出来。
*/
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
/* 3
* 创建一个长度为<小于等于c.size的最大的2的幂>的数组
* 然后将c中的元素添加到elements中。
*/
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
/
addAll(c);
}
allocateElements
//构造一个长度为<严格大于numElements的最小的2的幂>的一个数组
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
//返回严格大于numElements的最小的2的幂 (当numElements小于8时,返回8)
private static int calculateSize(int numElements) {
//MIN_INITIAL_CAPACITY = 8
int initialCapacity = MIN_INITIAL_CAPACITY;
//当numElements大于等于8时,计算出大于numElements的最小的2的幂
if (numElements >= initialCapacity) {
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
//这里如果numElements小于8时,直接返回8.
return initialCapacity;
}
3.核心方法详解
3.1入队
- 入队有两种方式,从队列头或者从队列尾;
- 如果容量不够了,直接扩大为两倍;
- 通过取模的方式让头尾指针在数组范围内循环;
- x & (len - 1) = x % len,使用&的方式更快;
//从队头入队
public void addFirst(E e) {
//元素不允许为NULL
if (e == null)
throw new NullPointerException();
/*
* 因为element.length一定是2的幂,2的幂-1的二进制从低位起是一串1,高位都是0
* 初始时head = 0, 0 - 1 = - 1 ,-1 & 15 = 15, 此时head = 15
* 下一次 15 - 1 = 14, 14 & 15 = 14 此时head = 14
* 在下一次 14 - 1 = 13, 13 & 15 = 13,此时head = 13
* ......
* 最终当数组未满时,会循环回来。
* 即head指向的是当前队头元素。
*/
elements[head = (head - 1) & (elements.length - 1)] = e;
//tail指向的是头元素的下一个位置。判断head == tail即判断数组是否满了,需要扩容。
if (head == tail)
//从方法名可以看出,扩容为原数组长度2倍。
doubleCapacity();
}
// 从队尾入队
public void addLast(E e) {
// 不允许null元素
if (e == null)
throw new NullPointerException();
//初始时tail为0,直接入队,此时tail指向的是从队尾入队队列的头元素的下一个位置。
elements[tail] = e;
/*
* head指向的是队头元素的位置
* tail + 1指向队头的下一个元素,判断是否 == head,即判断数组是否满了。
* 即是否走扩容的逻辑。
*/
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
3.2扩容
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];
// 将旧数组head之后的元素拷贝到新数组中
System.arraycopy(elements, p, a, 0, r);
// 将旧数组下标0到head之间的元素拷贝到新数组中
System.arraycopy(elements, 0, a, r, p);
// 赋值为新数组
elements = a;
// head指向0,tail指向旧数组长度表示的位置
head = 0;
tail = n;
}
元素迁移示意图
3.3出队
// 从队列头出队
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
// 取队列头元素 (head指向的就是头元素)
E result = (E) elements[h];
// 如果队列为空,就返回null
if (result == null)
return null;
// 将队列头置为空
elements[h] = null;
// 队列头指针右移一位
head = (h + 1) & (elements.length - 1);
// 返回取得的元素
return result;
}
// 从队列尾出队
public E pollLast() {
// 尾指针左移一位 因为通过addLast()我们可以知道,tail指向的是头元素的下一个位置
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
// 取当前尾指针处元素
E result = (E) elements[t];
// 如果队列为空返回null
if (result == null)
return null;
// 将当前尾指针处置为空
elements[t] = null;
// tail指向新的尾指针处
tail = t;
// 返回取得的元素
return result;
}
3.4作为栈使用
//头部入队
public void push(E e) {
addFirst(e);
}
//头部出队
public E pop() {
//调用的还是pollFirst()
return removeFirst();
}
4.总结
(1)ArrayDeque是采用数组方式实现的双端队列;
(2)ArrayDeque的出队入队是通过头尾指针循环利用数组实现的;
(3)ArrayDeque容量不足时是会扩容的,每次扩容容量增加一倍
(4)ArrayDeque可以直接作为栈使用;
双端队列与双重队列
双端队列与双重队列?
- 双端队列(Deque)是指队列的两端都可以进出元素的队列,里面存储的是实实在在的元素。
- 双重队列(Dual Queue)是指一种队列有两种用途,里面的节点分为数据节点和非数据节点,它是LinkedTransferQueue使用的数据结构。