事情是這樣的
在一次開發當中,我想用到隊列的相關特性,這次隊列不用考慮在併發情況下的安全特性,併發包裏面的數據結構就不用考慮了。於是我找啊找,到了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接口的實現來分析,就是說一端入隊一端出隊的結構來分析。
1.ArrayDeque 的構造
當我們我們初始化一個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];
}
2.ArrayDeque 入隊操作
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)部分,當執行這串代碼的時候會擴容
當程序跑到第(1)部分時,已經入隊了7個元素此時,
繼續入隊8會發生擴容:
deque.offer(8);此時這個操作會判斷tail == head 將當前容量設置爲雙倍容量
3.ArrayDeque 出隊操作
我們執行如下操作
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;
}
4.ArrayDeque的一些訪問操作
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;
}
5.ArrayDeque 其他的一些操作
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;
}