在一次开发当中,我想用到队列的相关特性,这次队列不用考虑在并发情况下的安全特性,并发包里面的数据结构就不用考虑了。于是我找啊找,到了jdk中队列的实现有LinkedList,ArrayDeque,PriorityQueue。因为这次使用不需要考虑优先级,首先就排除了PriorityQueue。剩下LinkedList,ArrayDeque了。很纠结啊,到底用谁呢,LinkdList很熟悉,是一个双向链表,对于头部和尾部的增删操作支持都非常好,再看看ArrayDeque,他认识我,我不是很认识它,我就带着好奇的心态去详细的了解这个结构。
看到ArrayDeque 描述说,如果你想把LinkedList当作队列来用,那么ArrayDeque会更快。哈哈,正合我意,为什么这么快呢?让我们来详细了解这个结构吧。首先ArrayDeque 是一个用数组实现的没有容量限制的双端队列。所谓双端队列,是可以在队列的头部和尾部都进行进行如添加和删除操作的队列。ArrayDeque继承了AbstractCollection,实现了Deque。ArrayDeque的增删操作都是围绕head下标和tail下标来对数组进行操作的。对一块连续的内存进行读取,设置某内存的值都是很快的。
优缺点:
1.没有容量限制。
2.多线程环境下不支持并发访问。
3.不支持插入空元素。
4.当把LinkedList 用做queue 的时候,把Stack 用做stack 时,ArrayDeque 速度会比他们更快。
当我们分别用ArrayDeque和LinkedList 分别对1百万个数进行入队,出队的操作时,他们所花费的时间:
package java_util_t;
import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Queue;
public class ArrayDequeDemo {
/**
* @param args
*/
public static void main(String[] args) {
int time = 1000000;
long s1 = System.currentTimeMillis();
Queue queue1 = new ArrayDeque(time);
for (int i = 0; i < time; i++) {
queue1.offer(i);
}
for (int i = 0; i < time; i++) {
queue1.poll();
}
System.out.println("ArrayDeque cost time:" + (System.currentTimeMillis() - s1));
long s2 = System.currentTimeMillis();
Queue queue2 = new LinkedList();
for (int i = 0; i < time; i++) {
queue2.offer(i);
}
for (int i = 0; i < time; i++) {
queue2.poll();
}
System.out.println("LinkedList cost time:" + (System.currentTimeMillis() - s2));
}
}
对于ArrayDeque源码的分析主要从Queue接口的实现来分析,就是说一端入队一端出队的结构来分析。
当我们我们初始化一个Queue
public ArrayDeque() {//底层默认大小为16 的Object 数组
elements = (E[]) new Object[16];
}
public ArrayDeque(int numElements) {//构造一个 容量>= numElients 的队列
allocateElements(numElements);//分配底层数组元素,默认为null
}
private void allocateElements(int numElements) {//分配空元素
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) {//这里设计很巧妙,会寻找一个>numElements 的2的n次幂的一个初始容量,如果数值越界了导致出现了负数,就给个2^30
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
}
elements = (E[]) new Object[initialCapacity];
}
Queue
deque.offer(1);
当我们执行这个操作的时候:
public boolean offer(E e) {//队列入队操作
return offerLast(e);//队列尾部入队操作
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public void addLast(E e) {
if (e == null)//不允许插入Null元素,否则抛出异常
throw new NullPointerException();
elements[tail] = e;//设置tail下标元素
if ( (tail = (tail + 1) & (elements.length - 1)) == head)//你可以把数组队列想象成一个环形数组,当tail+1超过数组最后一个下标时,&操作返回0,此时tail ==head
doubleCapacity();//扩容
}
private void doubleCapacity() { //入队时,如果队列元素为空,就会扩容
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p;
int newCapacity = n << 1;//构造或者上一次扩容时队列的容量总是为2^n,当扩充容量为当前容量的2倍时,只需一个左移操作即可。
if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r);//把旧队列head下标到(element.length-1下标)的元素复制到新队列的开头。(1)部分
System.arraycopy(elements, 0, a, r, p);//复制0~head下标之间的元素到到新队列的(1)部分之后。
elements = (E[])a; //替换旧的元素数组
head = 0;//重新设置头结点
tail = n;//重新设置尾节点
}
扩容图示:
当我们执行以下代码时,会发生扩容
Queue deque = new ArrayDeque(6);
for (int i = 1; i <= 7; i++) {
deque.offer(i);
}
//第(1)部分
deque.offer(8);//第(2)部分,当执行这串代码的时候会扩容
当程序跑到第(1)部分时,已经入队了7个元素此时,
继续入队8会发生扩容:
deque.offer(8);此时这个操作会判断tail == head 将当前容量设置为双倍容量
我们执行如下操作
Queue
deque.offer(1);
deque.poll();
当出队的时候:
public E poll() {//队列出队
return pollFirst();//出队第一个元素
}
public E pollFirst() {
int h = head;
E result = elements[h]; //出队操作时,会返回第一个元素给调用者
if (result == null)
return null;
elements[h] = null; //出队操作的实质是通过将队列的头元素设置为null,然后再将head下标往后移动一位。
head = (h + 1) & (elements.length - 1);//这里就是上一步说的将head下标往后移动一位,
//而这个head = (h + 1) & (elements.length - 1) 操作保证head在往后移动的时候不会数组越界
return result;
}
public E element() {//查看队列头元素,其实跟peek()唯一不同的是,当队列为空时,element()会抛出NoSuchElementException
return getFirst();//获取头元素
}
public E getFirst() {
E x = elements[head];//直接返回数组head下标指向的元素
if (x == null)
throw new NoSuchElementException();
return x;
}
public E peek() {//查看队列头元素,当队列为空时直接返回null,不抛异常
return peekFirst();
}
public E peekFirst() {
return elements[head]; // 如果队列为空时,返回elments[head] = null;
}
public void clear() {//清空操作
int h = head;
int t = tail;
if (h != t) { // head != tail 表示队列元素 不为空
head = tail = 0;//设置head 和 tail 初始状态
int i = h;
int mask = elements.length - 1;
do {
elements[i] = null;//配合循环将所有元素设置为null
i = (i + 1) & mask;
} while (i != t);
}
}
public boolean contains(Object o) {//判断队列是否包含该元素
if (o == null)
return false;
int mask = elements.length - 1;
int i = head;
E x;
while ( (x = elements[i]) != null) {//从head元素向后猪哥判断,是否equals
if (o.equals(x))
return true;
i = (i + 1) & mask;
}
return false;
}
public int size() {//获取队列元素个数,(tail - head) & (elements.length - 1)保证大小在有效范围内。
return (tail - head) & (elements.length - 1);
}
public boolean isEmpty() {//入队操作,tail+=1;出队操作head+=1;当一直出队元素的时候,head一直+,会==tail,此时head==tail都指向null元素。
return head == tail;
}