最近在研讀OkHttp源碼,發現它的Dispatcher分發器使用了ArrayDeque數據集合,這個集合類是java.util裏提供的雙端隊列/棧,非線程安全,可是性能很好,很是值得研究一下。javascript
咱們先總體推測一下ArrayDeque的設計思路。java
每一種集合類的具體功能,基本上都是增/刪/改/查四大項,ArrayDeque也不例外,只不過它更貼近於棧的功能,主要操做棧頂和棧底的數據,對於棧中的數據,只有delete一個函數。數組
Java中通常只有兩種基礎的數據容器,數組或鏈表。數組排列緊密、下標查找快、中間插入慢;鏈表排列稀疏、查找慢,中間插入快。對於棧結構來講,採用數組更具優點,ArrayDeque也是採用了數組來做爲基本數據容器,也須要處理數組擴容問題。安全
不過,傳統的數組都是線性數組,向尾部添加數據當然很方便,可是向中間添加數據的話,就須要挪動插入點以後的全部數據,若是向頭部添加數據的話,整個數組都要挪一遍,雖然Java提供了Native函數System.arrayCopy來提高效率,可是對內存的操做是不可避免的。bash
爲了提升效率,ArrayDeque採用了循環數組的設計,也就是說雖然基礎容器是一個普通的數組(默認容量16),可是在邏輯上,這個數組沒有固定的開頭或結尾,既能夠直接向尾部添加數據,也能夠直接向頭部以前添加數據,不須要大面積地移動數據。
邏輯上的概念大概是這樣的:markdown
循環數組在概念上沒有左右邊界,可是Java並無這樣的數組,Java只能提供固定大小的數組,這樣的話,如何實現循環數組就轉變爲如何利用固定數組實現循環數組。
相對於線性數組,循環數組是連續的,可是數組的頭和尾可能在任何位置,因此循環數組在真實數據中的映射大概是這樣:函數
主要判斷頭的新位置在左側仍是右側。
先看添加:
head的新位置必定是head-1,若是獲得的結果爲-1,就須要挪到右側,也就是物理數組中的最後一個位置length-1。
若是讓咱們本身來寫,多是這樣寫:工具
if(head-1<0){ head=elements.length -1; }else{ head=head-1; } elements[head]=e; 複製代碼
事實上,ArrayDeque使用了更精妙的實現,他用一步位與運算實現了這個功能:性能
elements[head = (head - 1) & (elements.length - 1)] = e;
複製代碼
這行代碼具體什麼意思呢?
當head-1爲-1時,其實是11111111&00001111,結果是00001111,也就是物理數組的尾部15;
當head-1爲較小的值如3時,其實是00000011&00001111,結果是00000011,仍是3。
當head增加如head+1超過物理數組長度如16時,其實是00010000&00001111,結果是00000000,也就是0,這樣就回到了物理數組的頭部。
因此,位與運算能夠很輕鬆地實現把數據控制在某個範圍內。
回過頭來,咱們再看刪除頭的代碼:優化
elements[h] = null; // Must null out slot head = (h + 1) & (elements.length - 1); 複製代碼
先清空數據,而後移動head位置,也是用位與運算實現的。
位運算原本就很是高效,ArrayDeque的這種寫法,更是用一行代碼覆蓋了函數中的全部場景,很是精妙。
主要判斷尾的新位置在左側仍是右側,具體操做和上一步添加/刪除頭相似。
刪除中間元素時,即便是循環數組,也須要批量移動數組元素了,因此刪除中間元素實際上面臨三個問題,一是須要在左側或右側刪除,二是須要挪動頭或尾,三是優化須要,儘可能少得移動數組元素。
ArrayDeque其實是先從第三個問題入手的,先判斷中間元素裏head近仍是離tail近,而後移動較近的那一端。
不過,較近的一端只是邏輯上較近,物理數組上,可能被分紅了兩截,這就須要作兩次數組元素的批量移動。
//ArrayDeque源碼(部分狀況) System.arraycopy(elements, 0, elements, 1, i); elements[0] = elements[mask]; System.arraycopy(elements, h, elements, h + 1, mask - h); 複製代碼
若是讓咱們本身寫,可能也是分條件判斷,分別計算兩截的數據。
ArrayDeque又使用了位與運算:
return (tail - head) & (elements.length - 1); 複製代碼
也是隻有一行,當物理上被分爲兩截時,tail-head會是負數,整個操做至關於取模運算,例如,當tail爲3,head爲14,物理數組長度16時,運算的就是11110101&00001111,值爲00000101,也就是5。
從基本操做上,若是物理上不連續,先複製右側,再補上左側。
toArray的問題在於新數組的長度是動態的,爲了生成大小恰好的新數組,ArrayDeque使用了Arrays工具類來實現這個特定長度的數組,並同時實現對head一側數據的複製:
boolean wrap = (tail < head); int end = wrap ? tail + elements.length : tail; Object[] a = Arrays.copyOfRange(elements, head, end); if (wrap) System.arraycopy(elements, 0, a, elements.length - head, tail); 複製代碼
這樣的話,若是物理空間連續,就直接複製完成;
但若是物理空間不連續,第一次Arrays.copyOfRange須要保證一次性生成足夠的物理空間,因此end的值不能是length(不然長度就只有length-head這麼長),而應該是tail+length。
數據容器的擴容其實能夠分解爲兩個問題,一是什麼時候開始擴容,二是舊數組數據的複製。
對於什麼時候開始擴容的問題,爲了減小檢查的次數,ArrayDeque採用了對head和tail是否重合的檢查,只要tail和head不重合,就說明tail後面/head前面還有空間,因此只要在添加頭/尾時檢查head==tail便可。
對於舊數組數據的複製,空間擴容和問題和toArray有些相似,不一樣的是新數組的長度是可知的(2倍),因此能夠直接new一個定長的數組,這樣就不須要Array.copyOfRange函數,能夠統一使用System.arrayCopy:
Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); 複製代碼
相比ArrayList,咱們能夠看到ArrayDeque大量減小了System.arrayCopy的使用,只在delete、clone、擴容和toArray函數中使用了這個函數,其餘操做中都不須要大量移動數組元素,這也能夠說明ArrayDeque這個數據集合的性能很是優良。