本文對Java中的ArrayDeque集合的源碼進行了深度解析,包括各種方法的底層實現,在最後給出了ArrayDeque和LinkedList的對比案例以及使用建議。
1 ArrayDeque的概述
public class ArrayDeque< E >
extends AbstractCollection< E >
implements Deque< E >, Cloneable, Serializable
ArrayDeque,來自於JDK1.6,底層是採用可變容量的環形數組實現的一個雙端隊列,內部元素有序(存放順序),不支持null元素(LinkedList是支持的)。
繼承了AbstractCollection,屬於Java集合體系中的一員,具有Collection體系集合的通用方法,比如通過iterator()獲取iterator迭代器方法。
沒有實現List接口,不具有通過listiterator()獲取更高級的listiterator迭代器的方法,同時不具有通過索引操作元素的一系列方法,例如add(int index, E element),get(int index),remove(int index),set(int index, E element)等一系列方法都沒有!
實現了Deque接口,即“double ended queue,雙端隊列”,因此具有在雙端隊列兩端訪問元素的方法,同時可以模擬“先進先出FIFO”的隊列,也可以模擬“先進後出FILO”的棧。
實現了Cloneable、Serializable標誌性接口,支持克隆、序列化操作。
ArrayDeque的實現不是同步的,但可以使用Collections.synchronizedCollection()來轉換成線程安全的Collection!
ArrayDequed的學習通常伴隨着和LinkedList的對比,關於LinkedList可以看這篇文章:Java的LinkedList集合源碼深度解析以及應用介紹。
2 ArrayDeque的API方法
和LinkedList一樣,ArrayDeque由於實現了Deque接口,擁有幾套操作鏈表頭尾的方法,比如插入、獲取、移除,同樣有一些方法會拋出異常,有些是返回一個特殊的值比如null:
行爲 | 第一個元素(頭部) | 最後一個元素(尾部) | ||
結果 | 拋出異常 | 特殊值 | 拋出異常 | 特殊值 |
插入 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
移除 | removeFirst() | pollFirst() | removeLast() | pollLast() |
獲取 | getFirst() | peekFirst() | getLast() | peekLast() |
由於Deque接口繼承了 Queue 接口。在將雙端隊列用作隊列時,將得到 FIFO(先進先出)行爲。將元素添加到雙端隊列的末尾,從雙端隊列的開頭移除元素。從 Queue 接口繼承的方法完全等效於 Deque 方法,如下表所示:
Queue 方法 | 等效 Deque 方法 |
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
雙端隊列Deque也可用作 LIFO(後進先出)棧,應優先使用此接口而不是遺留 Stack 類(關於Stack 的解析可以看:Java的Stack集合源碼深度解析以及應用介紹)。 在將雙端隊列用作棧時,元素被推入雙端隊列的開頭並從雙端隊列開頭彈出。棧方法完全等效於 Deque 方法,如下表所示:
堆棧方法 | 等效 Deque 方法 |
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
無論是隊列或者棧,都從雙端隊列的開頭抽取元素。
3 ArrayDeque的源碼解析
3.1 主要類屬性
/**
* 內部存儲元素的數組,即ArrayDeque採用數組實現的雙端隊列
*/
transient Object[] elements;
/**
* 雙端隊列的頭節點索引
*/
transient int head;
/**
* 雙端隊列的尾節點的下一位的索引,即下一個將要被加入到尾部的元素的索引。
*/
transient int tail;
/**
* 新創建的集合使用的最小容量。 容量,一定是2的冪
*/
private static final int MIN_INITIAL_CAPACITY = 8;
從這幾個主要屬性能夠看出來,ArrayDeque使用數組來實現雙端隊列,並且將數組的索引當作隊頭或者隊尾的下一位,並且最小容量爲8,並且容量必須是2的冪次方。
3.2 構造器與初始容量
3.2.1 ArrayDeque()
public ArrayDeque()
構造一個初始容量爲16的空數組雙端隊列。
public ArrayDeque() {
//很簡單,創建一個長度16的空數組
elements = new Object[16];
}
3.2.2 ArrayDeque(int numElements)
public ArrayDeque(int numElements)
構造一個指定初始容量的空數組雙端隊列。
private void allocateElements(int numElements) {
//獲取固定的最小容量
int initialCapacity = MIN_INITIAL_CAPACITY;
// 如果指定容量大於等於固定的最小容量
// 那麼通過下面的算法 嘗試尋找 大於指定容量的最小的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
}
//如果指定容量小於固定的最小容量,那麼初始容量就是固定的最小容量8
//根據最終的容量創建數組
elements = new Object[initialCapacity];
}
我們重點看看尋找真正初始化容量的算法!
第一行是initialCapacity = numElements,將指定容量的值賦值給初始容量;
然後是接下來六行:
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
這是一個很巧妙的算法,對於傳入的任何一個小於230(大於等於8)的int類型正整數值initialCapacity,經過五次無符號右移和位或操作後,將會得到一個2k-1的值,最後再自增1,得到2k,該2k就是大於initialCapacity的最小的2的冪次方。
|= 是“位或等”的的連寫,a|=b,即表示a=a|b。這裏的|是位或運算符,在計算時先將十進制數轉換爲二進制補碼,位數對齊,兩個位數只要有一個爲1,結果就爲1,否則結果爲0。
>>> 表示無符號右移,a>>>n,表示數a右移n位,包括符號位。即先將十進制數a轉換爲二進制補碼,然後帶着符號位向右邊移動n位,左邊的空位都以0補齊,這裏的無符號右移a>>>n可以表示爲a=a/2^n
這裏可能比較可抽象,我們來看案例,假如最開始的initialCapacity是8。
8的二進制補碼:0000 0000 0000 0000 0000 0000 0000 1000
>>>1之後變成: 0000 0000 0000 0000 0000 0000 0000 0100
|運算之後變成:0000 0000 0000 0000 0000 0000 0000 1100
>>>2之後變成: 0000 0000 0000 0000 0000 0000 0000 0011
|運算之後變成:0000 0000 0000 0000 0000 0000 0000 1111
>>>4之後變成: 0000 0000 0000 0000 0000 0000 0000 0000
|運算之後變成:0000 0000 0000 0000 0000 0000 0000 1111
>>>8之後變成: 0000 0000 0000 0000 0000 0000 0000 0000
|運算之後變成:0000 0000 0000 0000 0000 0000 0000 1111
>>>16之後變成: 0000 0000 0000 0000 0000 0000 0000 0000
|運算之後變成:0000 0000 0000 0000 0000 0000 0000 1111
自增1之後變成:0000 0000 0000 0000 0000 0000 0001 0000
右移運算和位或運算之後的最終結果轉換爲十進制就是15,然後再自增1,變成16,即計算出大於8的最小2次冪爲16。
實際上該算法的核心就是通過右移和或運算把這個數的最高位1的所有低位全部轉換爲1。由於在java中整型是固定的32位,我們可以看到5次右移操作一共右移了32位數,這樣對於所以int類型範圍內的正整數的最高位1的所有低位全部都可以變爲1。
然後再加上1,二進制向前進位1,後面的所有低位變成0,這樣就獲取了大於n的最小2次冪。
後面的代碼則是考慮到的特殊情況:
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
如果指定初始值是一個特別大的(大於等於230)int類型的值,那麼它的二進制補碼爲 “01 ……”,這樣在經過上面的算法之後,得到的值肯定是0111 1111 1111 1111 1111 1111 1111 1111。後面再自增1,這樣就變成了1000 0000 0000 0000 0000 0000 0000 0000。
我們可以看到,最後的結果最高位符號位變成了1,這就變成了一個負數,出現錯誤,實際上這是由於超過了int值的最大限制導致的。對於這種情況,ArrayDeque的做法是將該initialCapacity無符號右移一位,這樣它就變成了0100 0000 0000 0000 0000 0000 0000 0000,轉換爲10進制就是230,這實際上就是int類型範圍內的2次冪的最大值(Java的int類型範圍是-231 ~ 231-1)。
總結:
對於指定容量的構造器,它的實際容量並不一定是指定容量,分爲三種情況:
- 如果指定容量numElements小於MIN_INITIAL_CAPACITY,那麼實際初始化容量爲MIN_INITIAL_CAPACITY;
- 如果指定容量numElements大於等於MIN_INITIAL_CAPACITY且小於230,那麼實際初始化容量爲大於numElements的最小二次冪;
- 如果指定容量numElements大於等於230,那麼實際初始化容量爲230;
補充: 實際上Hashmap的源碼中對於指定初始容量的計算也用到了類似的算法。另外,爲什麼初始容量一定是2的冪次方呢?下面會有解答!
3.2.3 ArrayDeque(Collection<? extends E> c)
public ArrayDeque(Collection<? extends E> c)
構造一個包含指定 collection 的元素的雙端隊列,這些元素按 collection 的迭代器返回的順序排列。
public ArrayDeque(Collection<? extends E> c) {
//分配足夠容量的空數組
allocateElements(c.size());
//批量添加
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
//內部是循環調用的add方法
for (E e : c)
if (add(e))
modified = true;
return modified;
}
3.3 添加的方法
3.3.1 添加到尾部的方法
public void addLast(E e)
將指定元素插入此雙端隊列的末尾。
/**
* 將指定元素添加到雙端隊列的末尾。
*
* @param e 需要被添加的元素
*/
public void addLast(E e) {
//如果被添加的元素爲null,則拋出NullPointerException異常
if (e == null)
throw new NullPointerException();
//將元素存放在tail位置,即原尾節點的下一個空節點位置
elements[tail] = e;
/*計算新的下一個尾節點索引*/
if ((tail = (tail + 1) & (elements.length - 1)) == head)
//擴容的方法
doubleCapacity();
}
首先,如果被添加的元素爲null,則拋出NullPointerException異常;然後插入元素;接着計算新的下一個尾節點索引,並且判斷是否需要擴容。
主要看計算新的下一個尾節點索引的算法和擴容機制!
3.3.1.1 計算新索引
首先,說明一下使用數組實現隊列的傳統方法一個缺點,即“假溢出”。對於傳統數組實現隊列的方式,一般來說頭節點和尾節點位於數組的頭尾部分,如果尾節點達到了數組末尾,直接增加尾節點索引將會導致數組長度溢出,因此常常考慮擴容。但是此時該數組的前半部分還有剩餘空間,例如在插入尾節點時刪除了一些頭節點),這種現象稱爲"假溢出",因爲真實的數組空間並沒有用完,造成了空間的浪費。關於此在Java中的隊列數據結構詳解以及實現案例演示一文中有更加詳細的解釋!
爲了優化這種“假溢出”的缺點。我們可以取消頭、尾節點的相對先後位置,例如,如果尾節點達到了數組末尾,先不急着擴容數組,此時將下一個尾節點的索引置爲0索引,即數組的開頭,然後判斷此時頭索引和下一個尾節點索引是否重合,如果沒有重合,那麼說明該數組還有可以利用的空間,如果重合那麼則可以擴容,這樣就能利用前半部分的空閒空間了。
這種解決方法得到的隊列的頭、尾節點索引,與數組索引的頭、尾沒有聯繫,頭節點索引可能比尾節點索引值更大,該數組被“循環利用”!
在ArrayDeque中,尋找下一個尾節點的操作就實現了上面的解決算法。並且它的實現也非常的巧妙,少量的代碼就能夠計算出新的下一個尾節點的索引的同時還能處理頭、尾索引達到數組兩端時的情況,最終得到的tail值是將會被固定在[0 , elements.length-1]之間。它的算法實現如下:
tail = (tail + 1) & (elements.length - 1)
&表示“位與”運算,它會將十進制數轉換爲二進制補碼,然後如果對位都是1,結果爲1,否則結果爲0,例如當elements.length =8時,tail=7時,下一個尾節點索引將會是8,此時計算出的值應該變成0,8&7:
0000 1000 -->8
0000 0111 -->7
—————————
0000 0000 -->0
這裏還需要解釋:爲什麼數組的初始容量一定是2的冪次方?
一個正整數如果2的冪次方,那麼該數的二進制一定是 “最高位是1,低位全是0”的形式;如果該數減去1之後,它的二進制變成“最高位降低一位,並且低位全部變成了1”的形式,上面的8和7,的二進制是1000,7的二進制是0111。
如果我們的elements.length是2的冪次方,那麼elements.length-1的二進制就會變成上面所說的低位全部變成1的情況。例如,如果elements.length爲8,某一時刻tail爲3,那麼下一個尾部應該爲4,經過&計算之後:
0000 0100 -->4
0000 0111 -->7
————————
0000 0100 -->4
還是得到4,我們可以發現,對於第一個操作數tail+1(1~ elements.length),(tail + 1) & (elements.length - 1)的計算結果能夠得到0~ elements.length-1的全部結果。
如果我們的elements.length不是2的冪次方,那麼elements.length-1的二進制就會變成“最高位降低一位,並且低位全部變成了1”的形式。
例如,如果elements.length爲9,某一時刻tail爲3,那麼下一個尾部應該爲4,經過&計算之後:
0000 0100 -->4
0000 1000 -->8
——————————
0000 0000 -->0
變成了0,我們可以發現,對於第一個操作數tail+1(1~ elements.length),(tail + 1) & (elements.length - 1)的計算結果不能夠得到0~ elements.length-1的全部結果,這樣的話計算出來的下一個尾節點索引就會有問題。
實際上這裏有一個計算規律。對於正整數m,n:
- 如果n=m,那麼m&n=n;
- 如果n爲2的冪次方,且m<n,那麼m&n=0;
- 如果n不爲2的冪次方,且m<n,那麼m&n=m;
- 如果n爲2的冪次方,那麼n+1&n=n;
- 如果n不爲2的冪次方,那麼n+1&n=0;
如果elements.length爲2的冪次方,那麼elements.length-1自然就不是2的冪次方。根據上面的計算規律,“數組容量(長度)必須是2的冪次方”這一要求是爲了保證計算下一個尾節點索引和頭節點索引的值的正確性,即爲了保證計算出的索引能夠取到[0~elements.length-1]之間的全部值!
3.3.1.2 擴容機制
在通過上面的算法計算出新尾節點的下一個索引tail之後,我們看到該值將會與head進行比較,如果相等,即下一個尾節點和頭節點索引重合了,那麼說明說組容量真正的用完了,此時需要進行擴容。
擴容的方法就是doubleCapacity方法,從名字上能夠看出來,擴容增量是原容量的一倍,即變成原容量的兩倍,我們來看具體源碼:
/**
* 擴容方法,儘量擴容爲原容量的兩倍,但是沒那麼簡單
*/
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
//新容量等於原容量左移一位,轉換爲十進制計算就是:newCapacity=n*(2^1)
int newCapacity = n << 1;
//如果新容量小於0,這裏肯定又是超過int值上限之後變成負數的情況了,從這裏能夠看出來原容量大於等於2^30之後,擴容後的容量就會小於0
if (newCapacity < 0)
//直接拋出異常
throw new IllegalStateException("Sorry, deque too big");
//newCapacity大於0,新建一個數組
Object[] a = new Object[newCapacity];
/*拷貝原數組的數據到新數組*/
//拷貝原頭節點右側的數據,索引爲[p,elements.length-1],到新數組索引爲[0,r-1]
System.arraycopy(elements, p, a, 0, r);
//拷貝原頭節點左側的數據,索引爲[0,p-1],到新數組索引爲[r,p-1]
System.arraycopy(elements, 0, a, r, p);
//通過上面的拷貝,將頭節點索引重新變成了0,下一個尾節點索引變成了n(即原數組的容量,因爲此時數組能夠容納該索引值了)
/*改變引用*/
elements = a;
//頭節點索引重新賦值
head = 0;
//下一個尾節點索引重新賦值
tail = n;
}
我們來總結一下擴容的主要步驟:
- 首先計算出新容量,使用<<運算計算出理論新容量應該爲原容量的兩倍或者爲負數,然後進入步驟2;實際上原容量大於等於230之後,擴容後的容量值就會小於0,即ArrayDeque最大容量爲230。
- 然後判斷新容量是否小於0,如果小於0那說明新容量超過了int值上限,拋出異常程序結束,如果大於0,那麼說明可以擴容,進入步驟3;
- 新建新容量大小的空數組,拷貝原數組的數據到新數組,通過兩次拷貝,將頭節點索引重新變成了0,下一個尾節點索引變成了原數組長度。然後改變數組引用和相關值,擴容結束!
3.3.1.3 其他方法
public boolean add(E e)
將指定元素添加到雙端隊列的末尾。
public boolean add(E e) {
//內部調用addLast的方法
addLast(e);
return true;
}
public boolean offerLast(E e)
將指定元素添加到雙端隊列的末尾。
public boolean offerLast(E e) {
//內部調用addLast的方法
addLast(e);
return true;
}
public boolean offer(E e)
將指定元素添加到雙端隊列的末尾。
public boolean offer(E e) {
//內部調用offerLast的方法
return offerLast(e);
}
3.3.2 添加到頭部的方法
public void addFirst(E e)
將指定元素插入此雙端隊列的開頭。
public void addFirst(E e) {
//判空
if (e == null)
throw new NullPointerException();
//計算新的頭節點索引並且插入元素
//這裏的計算方法和下一個尾節點索引計算方法差不多
elements[head = (head - 1) & (elements.length - 1)] = e;
//如果頭節點索引等於下一個尾節點索引,那麼嘗試擴容
if (head == tail)
doubleCapacity();
}
我們看看頭節索引的計算方法:
head = (head - 1) & (elements.length - 1)
該方法與下一個尾節點索引的計算方法差不多,不同之處在於這裏的第一個操作數是head-1,由於初始head是0,這樣當添加第一個數到頭節點時,就是-1&(elements.length - 1),該結果固定返回elements.length – 1,即頭節點索引是從數組尾部開始的,然後依次遞減;而前面的下一個尾節點索引的計算方法中,我們還記得第一個操作數是tail+1,即尾節點索引是從數組頭部開始的,依次遞增,這也是一個非常有趣的結果!
3.3.2.1 其他方法
public boolean offerFirst(E e)
將指定元素插入此雙端隊列的開頭。
public boolean offerFirst(E e) {
//內部調用addFirst方法
addFirst(e);
return true;
}
public void push(E e)
將元素推入此雙端隊列所表示的堆棧。換句話說,將元素插入此雙端隊列的開頭。
public void push(E e) {
//內部調用addFirst方法
addFirst(e);
}
3.4 移除的方法
3.4.1 移除尾部的方法
public E pollLast()
獲取並移除此雙端隊列的最後一個元素;如果此雙端隊列爲空,則返回 null。
public E pollLast() {
//由於tail表示尾節點的下一個節點索引,因此這裏獲取真正尾節點的索引
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
//獲取該索引處的節點元素result
E result = (E) elements[t];
//如果result爲null,則返回null
if (result == null)
return null;
//將該索引處空間置空
elements[t] = null;
//tail賦值爲該索引
tail = t;
//返回元素值
return result;
}
3.4.1.1 其他方法
public E removeLast()
獲取並移除此雙端隊列的最後一個元素;如果此雙端隊列爲空,它將拋出NoSuchElementException異常。
public E removeLast() {
//內部調用pollLast方法
E x = pollLast();
//如果返回值爲null,則拋出異常
if (x == null)
throw new NoSuchElementException();
return x;
}
3.4.2 移除頭部的方法
public E pollFirst()
獲取並移除此雙端隊列的第一個元素;如果此雙端隊列爲空,則返回 null。
public E pollFirst() {
//獲取頭結節點索引
int h = head;
@SuppressWarnings("unchecked")
//獲取該索引處的元素
E result = (E) elements[h];
// 如果該元素爲null,則返回null
if (result == null)
return null;
//該索引處空間置空
elements[h] = null;
//計算下一個新的頭節點索引
head = (h + 1) & (elements.length - 1);
//返回該元素節點
return result;
}
3.4.2.1 其他方法
public E removeFirst()
獲取並移除此雙端隊列第一個元素。如果此雙端隊列爲空,它將拋出NoSuchElementException異常。
public E removeFirst() {
//內部調用pollFirst方法
E x = pollFirst();
//如果返回null.則拋出異常
if (x == null)
throw new NoSuchElementException();
return x;
}
public E remove()
獲取並移除此雙端隊列第一個元素。如果此雙端隊列爲空,它將拋出NoSuchElementException異常。
public E remove() {
//內部調用removeFirst方法
return removeFirst();
}
public E poll()
獲取並移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素);如果此雙端隊列爲空,則返回 null。
public E poll() {
//內部調用pollFirst方法
return pollFirst();
}
3.4.3 移除指定元素
3.4.3.1 移除第一次出現的元素
public boolean removeFirstOccurrence(Object o)
從此雙端隊列移除第一次出現的指定元素。如果此雙端隊列不包含該元素,則不作更改。更確切地講,移除第一個滿足 (o == null ? e == null : o.equals(e)) 的元素 e(如果存在這樣的元素)。如果此雙端隊列包含指定的元素(或者此雙端隊列由於調用而發生了更改),則返回 true。
public boolean removeFirstOccurrence(Object o) {
//null檢查
if (o == null)
//如果o爲null,則返回false
return false;
//獲取最大索引
int mask = elements.length - 1;
//獲取頭節點索引
int i = head;
Object x;
//從頭節點開始循環查找
while ( (x = elements[i]) != null) {
//如果節點不爲null
//如果節點等於o,這裏使用equals進行比較的
if (o.equals(x)) {
/*調用delete刪除該位置的元素,並且調整後續元素的位置*/
delete(i);
//返回true
return true;
}
//索引i的值,類似於添加節點時的獲取下一個節點索引的處理方法,即循環查找
i = (i + 1) & mask;
}
//走到這一步,說明循環完畢,還是沒有找到相等的元素,返回false
return false;
}
我們來看delete方法源碼:
/**
* 刪除指定索引的元素,並且調節其他元素的位置
*
* @param i 索引
* @return 如果i到頭節點比較近則返回false ;如果i到尾節點比較近則返回true,該返回值在迭代器中會用到
*/
private boolean delete(int i) {
//一系列斷言,保證數據結構是正確的
checkInvariants();
//保存原數組的引用
final Object[] elements = this.elements;
//獲取最大索引,即&運算的第二個操作數
final int mask = elements.length - 1;
final int h = head;
final int t = tail;
//獲取i到頭節點索引的距離
final int front = (i - h) & mask;
//獲取i到尾節點索引的距離
final int back = (t - i) & mask;
// 檢測併發修改,如果front 大於等於尾節點到頭節點的距離,那麼說明出現了"併發修改",拋出異常
if (front >= ((t - h) & mask))
throw new ConcurrentModificationException();
/*需要移動元素數量優化
如果i到頭節點比較近,則將該位置到頭部之間的元素進行移動,如果i到尾節點比較近,則將該位置到尾部之間的元素進行移動
儘量減少移動的元素數量*/
/*1 如果i到頭節點比較近*/
if (front < back) {
if (h <= i) {
//如果i大於等於頭節點索引h,那麼直接arraycopy一次就行了,將[h,i-1]的元素移動至[h+1,i]的位置,如果h=i那麼說明不需要移動
System.arraycopy(elements, h, elements, h + 1, front);
} else {
//如果i小於頭節點索引h,那麼稍微複雜點,因爲出現了不連續的兩段元素,即[h,elements.length - 1]和[0,i-1],需要arraycopy兩次
//首先將[0,i-1]的元素移動至[1,i]的位置
System.arraycopy(elements, 0, elements, 1, i);
//然後將數組最後一個元素(elements.length - 1的索引位置處)移動至開頭(0索引位置處)
elements[0] = elements[mask];
//最後將[h,elements.length - 1- 1]的元素移動至[h+1,elements.length - 1]的位置
System.arraycopy(elements, h, elements, h + 1, mask - h);
}
//將原頭節點位置 置空
elements[h] = null;
//計算新的head索引
head = (h + 1) & mask;
//返回false
return false;
}
/*2 如果i到尾節點比較近*/
else {
if (i < t) {
//如果i小於尾節點的下一個節點索引t,那麼直接arraycopy一次就行了,將[i+1,t]的元素移動至[i,t-1]的位置
System.arraycopy(elements, i + 1, elements, i, back);
//計算新的尾節點的下一個節點索引,直接減少1就可以了,因此此時t肯定大於0,減去1不會是負數;同時不需要手動置空t的位置,因爲在上面移動時,最後一個元素爲null
tail = t - 1;
} else {
//如果i大於等於尾節點的下一個節點索引t,那麼稍微複雜點,因爲出現了不連續的兩段元素,即[i+1,elements.length - 1]和[0,t],需要arraycopy兩次
//首先將[i+1,elements.length - 1]的元素移動至[i,elements.length - 1 - 1]的位置
System.arraycopy(elements, i + 1, elements, i, mask - i);
//然後將數組第一個元素(0索引位置處)移動至末尾(elements.length - 1的索引位置處)
elements[mask] = elements[0];
//最後將[1,t]的元素移動至[0,t - 1]的位置
System.arraycopy(elements, 1, elements, 0, t);
//計算新的尾節點的下一個節點索引;同時不需要手動置空t的位置,因爲在上面移動時,最後一個元素爲null
tail = (t - 1) & mask;
}
//返回true
return true;
}
}
看起來很多,實際上也不是很難,主要注意的其中一個優化方案是:先計算出需要刪除的索引到頭節點和尾節點的距離,然後取較小的哪一段距離進行元素移動,這樣能減少需要移動的元素的個數。
public boolean remove(Object o)
從此雙端隊列中移除第一次出現的指定元素。
public boolean remove(Object o) {
//內部調用removeFirstOccurrence方法
return removeFirstOccurrence(o);
}
3.4.3.1 移除最後一次出現的元素
public boolean removeFirstOccurrence(Object o)
從此雙端隊列移除最後一次出現的指定元素。如果此雙端隊列不包含該元素,則不作更改。更確切地講,移除最後一個滿足 (o == null ? e == null : o.equals(e)) 的元素 e(如果存在這樣的元素)。如果此雙端隊列包含指定的元素(或者此雙端隊列由於調用而發生了更改),則返回 true。
public boolean removeLastOccurrence(Object o) {
//null檢查
if (o == null)
//如果o爲null,則返回false
return false;
//獲取最大索引
int mask = elements.length - 1;
//獲取尾節點的索引
int i = (tail - 1) & mask;
Object x;
//從尾節點開始循環查找
while ( (x = elements[i]) != null) {
//如果節點不爲null
//如果節點等於o,這裏使用equals進行比較的
if (o.equals(x)) {
/*調用delete刪除該位置的元素,並且調整後續元素的位置*/
delete(i);
return true;
}
//索引i的值,類似於添加節點時的獲取下一個節點索引的處理方法,即循環查找,這裏是遞減循環
i = (i - 1) & mask;
}
//走到這一步,說明循環完畢,還是沒有找到相等的元素,返回false
return false;
}
3.5 獲取的方法
3.5.1 獲取頭部的方法
public E getFirst()
獲取,但不移除此雙端隊列的最後一個元素。如果此雙端隊列爲空,它將拋出NoSuchElementException異常。
public E getFirst() {
//獲取頭節點
@SuppressWarnings("unchecked")
E result = (E) elements[head];
//如果頭節點爲null,則拋出異常
if (result == null)
throw new NoSuchElementException();
return result;
}
public E peekFirst()
獲取,但不移除此雙端隊列的最後一個元素;如果此雙端隊列爲空,則返回 null。
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}
pubilc E element()
獲取,但是不移除此隊列的頭。如果此隊列爲空,它將拋出NoSuchElementException異常。
public E element() {
//內部調用getFirst的方法
return getFirst();
}
public E peek()
獲取,但不移除此雙端隊列的第一個元素;如果此雙端隊列爲空,則返回 null。
public E peek() {
//內部調用peekFirst的方法
return peekFirst();
}
3.5.2 獲取尾部的方法
public E getLast()
獲取,但不移除此雙端隊列的最後一個元素。如果此雙端隊列爲空,它將拋出NoSuchElementException異常。
public E getLast() {
//獲取尾部元素
@SuppressWarnings("unchecked")
E result = (E) elements[(tail - 1) & (elements.length - 1)];
//如果尾節點爲null,則拋出異常
if (result == null)
throw new NoSuchElementException();
return result;
}
public E peekLast()
獲取,但不移除此雙端隊列的最後一個元素;如果此雙端隊列爲空,則返回 null。
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}
3.6 其他方法
public boolean contains(Object o)
如果此雙端隊列包含指定元素,則返回 true。更確切地講,當且僅當此雙端隊列至少包含一個滿足 o.equals(e) 的元素 e 時,返回 true。
public boolean contains(Object o) {
//判空
if (o == null)
return false;
int mask = elements.length - 1;
int i = head;
Object x;
//循環查找,內部使用equals判斷是是否相等
while ( (x = elements[i]) != null) {
if (o.equals(x))
return true;
i = (i + 1) & mask;
}
return false;
}
public void clear()
從此雙端隊列中移除所有元素。在此調用返回之後,該雙端隊列將爲空。
public void clear() {
int h = head;
int t = tail;
if (h != t) { // clear all cells
head = tail = 0;
int i = h;
int mask = elements.length - 1;
do {
//循環將每一個節點置空
elements[i] = null;
i = (i + 1) & mask;
} while (i != t);
}
}
4 ArrayDeque和LinkedList的區別與使用建議
相同點:
- ArrayDeque和LinkedList都實現了Deque接口,都是雙端隊列的實現,都具有操作隊列頭尾的一系列方法。
- 都是非線程安全的集合。
不同點:
- ArrayDeque來自JDK1.6,底層是採用數組實現的雙端隊列,而LinkedList來自JDK1.2,底層則是採用鏈表實現的雙端隊列。
- ArrayDeque不允許null元素,而LinkedList則允許null元素。
- 如果僅僅使用Deque的方法,即從隊列兩端操作元素,用作隊列或者棧,並且如果數據量比較大,一般來說ArrayDeque的效率要高於LinkedList,其效率更高的原因可能是ArrayDeque不需要創建節點對象,添加的元素直接作爲節點對象,LinkedList則需要對添加的元素進行包裝爲Node節點,並且還具有其他引用的賦值操作。
- LinkedList還同時實現了List接口,具有通過“索引”操作隊列數據的方法,雖然這裏的“索引”只是自己維護的索引,並非數組的索引,但該功能這是ArrayDeque所不具備的。如果需要對 隊列中間的進行元素的增、刪、改操作,那麼你只能使用LinkedList,因此LinkedList的應用(或者說可用方法)更加廣泛。
附,使用Deque的方法時的ArrayDeque和LinkedList的性能對比:
/**
* @author lx
*/
public class ArrayDequeTest2 {
static ArrayDeque<Integer> arrayDeque = new ArrayDeque<Integer>();
static LinkedList<Integer> linkedList = new LinkedList<Integer>();
public static long arrayDequeAdd() {
//開始時間
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 5000000; i++) {
arrayDeque.addLast(i);
arrayDeque.addFirst(i);
}
//結束時間
long endTime = System.currentTimeMillis();
//返回所用時間
return endTime - startTime;
}
public static long arrayDequeDel() {
//開始時間
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 5000000; i++) {
arrayDeque.pollFirst();
arrayDeque.pollLast();
}
//結束時間
long endTime = System.currentTimeMillis();
//返回所用時間
return endTime - startTime;
}
public static long linkedListAdd() {
//開始時間
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 5000000; i++) {
linkedList.addLast(i);
linkedList.addFirst(i);
}
//結束時間
long endTime = System.currentTimeMillis();
//返回所用時間
return endTime - startTime;
}
public static long linkedListDel() {
//開始時間
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 5000000; i++) {
linkedList.pollFirst();
linkedList.pollLast();
}
//結束時間
long endTime = System.currentTimeMillis();
//返回所用時間
return endTime - startTime;
}
public static void main(String[] args) throws InterruptedException {
Thread.sleep(100);
long time1 = arrayDequeAdd();
long time3 = arrayDequeDel();
System.out.println("arrayDeque添加元素所用時間====>" + time1);
System.out.println("arrayDeque刪除元素所用時間====>" + time3);
System.gc();
Thread.sleep(100);
long time2 = linkedListAdd();
long time4 = linkedListDel();
System.out.println("linkedList添加元素所用時間====>" + time2);
System.out.println("linkedList刪除元素所用時間====>" + time4);
}
}
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!