在一次开发当中,我想用到队列的相关特性,这次队列不用考虑在并发情况下的安全特性,并发包里面的数据结构就不用考虑了。于是我找啊找,到了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<Integer> queue1 = new ArrayDeque<Integer>(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<Integer> queue2 = new LinkedList<Integer>(); 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<Integer> deque = new ArrayDeque<Integer>(6); 时候。会默认选一个>6的最小的2^n。
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<Integer> deque = new ArrayDeque<Integer>(6);
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<Integer> deque = new ArrayDeque<Integer>(6); for (int i = 1; i <= 7; i++) { deque.offer(i); } //第(1)部分 deque.offer(8);//第(2)部分,当执行这串代码的时候会扩容
继续入队8会发生扩容:
deque.offer(8);此时这个操作会判断tail == head 将当前容量设置为双倍容量
我们执行如下操作
Queue<Integer> deque = new ArrayDeque<Integer>(6);
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; }