最近盯上了java collection
框架中一個類——ArrayDeque
。不少人可能沒用過甚至沒據說過這個類(i'm sorry,what's fu*k this?),畢竟你坐在面試官面前的時候,關於數組鏈表的掌握狀況,99%的可能性聽到問題會是:說說ArrayList和LinkedList的區別?
今天從ArrayDeque入手,換一個角度來檢驗下咱們是否真正掌握了數組、鏈表。java
不着急分析這個類的核心方法,先看下它的父類和接口,以便在Java Collection宇宙中找準它的定位,順帶從宏觀角度窺探下Java Collection框架設計。
面試
父類是AbstractCollection
,看下它的方法
segmentfault
add、addAll、remove、clear、iterator、size……是否是都很常見?你經常使用的xxList中,常常會看到這些方法吧?能夠說,AbstractCollection這個抽象類,是這種結構(數組、鏈表等等)的骨架!數組
首先是Queue接口,定義出了最基本的隊列功能:
框架
那麼Deque接口呢?
入眼各類xxFirst、xxLast,這種定義決定了它是雙端隊列的表明!this
相繼看了父類和接口,樓主你到底想表達啥?嘿嘿,別急,我再反問一個經典問題——抽象類和接口有什麼區別?
你可能會有各類回答,好比抽象類能本身有本身的實現之類的。不能說不對,但這種答案至關於只停留在奇技淫巧層面,未得正統。以設計角度來看,實際上是is-a(抽象類)和has-a(接口)的區別!spa
好比定義汽車AbstractCar,會規定有輪子有發動機能跑的就是汽車;各家廠商生產的汽車都逃不出這個範疇,甭管你是大衆寶馬瑪莎拉蒂。設計
有些汽車多了座椅加熱;有些增設了天窗打開功能。但這些功能都是加強型的,並非每種汽車都會有!指針
抽象類和接口合理的組合,就產生了奇妙的效果:既能保證種族(類)的結構,又能對其進行擴展(接口)。給出你們熟悉的ArrayList和LinkedList,仔細感覺下:
code
這種設計不只僅限於Java Collection,開源框架中也是如此,好比Spring IOC中的Context、Factory那部分……
迴歸到本文的主角 ArrayDeque,既然它實現了Deque,天然具有雙端隊列的特性。類名中的 Array姓氏,無時無刻不在提醒咱們,它是基於數組實現的。
類註釋中,有句話引發了個人注意:
/** * This class is likely to be faster than * {@link Stack} when used as a stack, and faster than {@link LinkedList} * when used as a queue. */
(Stack先無論)註釋中後半句說,ArrayDeque做爲隊列時比LinkedList快,看看它是怎麼辦到的!
三大屬性:
transient Object[] elements; //基於數組實現 transient int head; //頭指針 transient int tail; //尾巴指針
技術敏感的同窗已經能猜到它是怎麼實現的了:數組做爲基底,兩個指分指頭尾,插入刪除操做時移動指針;若是頭尾指針重合,則須要擴容……
下面看看源碼實現,是否和咱們猜想的一致。
private static final int MIN_INITIAL_CAPACITY = 8; // ****** Array allocation and resizing utilities ****** private static int calculateSize(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) { 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 } return initialCapacity; }
規定最小值MIN_INITIAL_CAPACITY = 8
,若是入參小於8,數組大小就定義成8;若是大於等於8,這一通右移是啥操做?假如咱們傳入了16,二進制10000,逐步分析下:
1.initialCapacity |= (initialCapacity >>> 1)
右移1位做|
操做,10000->01000,'或' 操做後11000
2.initialCapacity |= (initialCapacity >>> 2)
接上一步,右移2位做|
操做,11000->00110,'或' 操做後11110
3.initialCapacity |= (initialCapacity >>> 4)
接上一步,右移4位做|
操做,11110->00001,'或' 操做後 11111
……
後面就兩步都是11111 | 00000,結果就是 11111
4.initialCapacity++
二進制數11111,+1以後100000,轉換成十進制32
最終的負值判斷(用於處理超int正向範圍狀況),先不考慮。
結論:這些'或' 操做,最終獲得了大於入參的2的次冪中最小的一個。
底層數組始終是2的次冪,爲何如此?帶着這個問題繼續往下分析
// The main insertion and extraction methods are addFirst, // addLast, pollFirst, pollLast. The other methods are defined in // terms of these.
以上註釋有云,核心方法就4個,咱們從add方法入手。
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)
,玄機就在這裏。若是你對1.8的HashMap
足夠了解,就會知道hashmap的數組大小一樣始終是2的次冪。其中很重要的一個緣由就是:當lengh是2的次冪的時候,某數字 x 的操做 x & (length - 1)
等價於 x % length
,而對二進制的計算機來講 & 操做要比 % 操做效率更好!
並且head = (head - 1) & (elements.length - 1)
,(head初始值0)第一次就將head指針定位到數組末尾了。
畫圖分析下:
可見,head指針從後向前移動。
public void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); }
addLast和addFirst原理相同,只是addLast控制tail指針,從前向後移動!
上圖中再作一次add操做,指針將會重合。好比,再一次addFirst以後:
if (head == tail) 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 int newCapacity = n << 1; //左移,等價乘2,依然保持2的次冪 if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big"); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n; }
經過數組拷貝和從新調整指針,完成了擴容。
至於pollFirst、pollLast是addFirst、addLast的相反操做,原理類似,很少作分析。
回到那個問題:做爲隊列時,ArrayDeque效率爲何會比LinkedList更好?
我以爲由於LinkedList做爲隊列實現,新增修改要多修改節點的pre、next指針,且地址非連續,尋址時也會比array花更多時間。