【源】ArrayDeque,Collection框架中不起眼的一個類

最近盯上了java collection框架中一個類——ArrayDeque。不少人可能沒用過甚至沒據說過這個類(i'm sorry,what's fu*k this?),畢竟你坐在面試官面前的時候,關於數組鏈表的掌握狀況,99%的可能性聽到問題會是:說說ArrayList和LinkedList的區別?
今天從ArrayDeque入手,換一個角度來檢驗下咱們是否真正掌握了數組、鏈表。java

父類和接口

不着急分析這個類的核心方法,先看下它的父類和接口,以便在Java Collection宇宙中找準它的定位,順帶從宏觀角度窺探下Java Collection框架設計。
clipboard.png面試

父類

父類是AbstractCollection,看下它的方法
clipboard.pngsegmentfault

add、addAll、remove、clear、iterator、size……是否是都很常見?你經常使用的xxList中,常常會看到這些方法吧?能夠說,AbstractCollection這個抽象類,是這種結構(數組、鏈表等等)的骨架!數組

接口

首先是Queue接口,定義出了最基本的隊列功能:
clipboard.png框架

那麼Deque接口呢?
clipboard.png
入眼各類xxFirst、xxLast,這種定義決定了它是雙端隊列的表明!this

框架設計

相繼看了父類和接口,樓主你到底想表達啥?嘿嘿,別急,我再反問一個經典問題——抽象類和接口有什麼區別?
你可能會有各類回答,好比抽象類能本身有本身的實現之類的。不能說不對,但這種答案至關於只停留在奇技淫巧層面,未得正統。以設計角度來看,實際上是is-a(抽象類)和has-a(接口)的區別!spa

  • 抽象類至關於某一個種族的基石

好比定義汽車AbstractCar,會規定有輪子有發動機能跑的就是汽車;各家廠商生產的汽車都逃不出這個範疇,甭管你是大衆寶馬瑪莎拉蒂。設計

  • 接口則關注各類功能

有些汽車多了座椅加熱;有些增設了天窗打開功能。但這些功能都是加強型的,並非每種汽車都會有!指針

抽象類和接口合理的組合,就產生了奇妙的效果:既能保證種族(類)的結構,又能對其進行擴展(接口)。給出你們熟悉的ArrayList和LinkedList,仔細感覺下:
clipboard.pngcode

這種設計不只僅限於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方法入手。

插入

  • addFirst
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指針定位到數組末尾了。

畫圖分析下:
clipboard.png

可見,head指針從後向前移動。

  • addLast
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

clipboard.png

addLast和addFirst原理相同,只是addLast控制tail指針,從前向後移動!

上圖中再作一次add操做,指針將會重合。好比,再一次addFirst以後:
clipboard.png

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;
}

clipboard.png
經過數組拷貝和從新調整指針,完成了擴容。

至於pollFirst、pollLast是addFirst、addLast的相反操做,原理類似,很少作分析。

回到那個問題:做爲隊列時,ArrayDeque效率爲何會比LinkedList更好?
我以爲由於LinkedList做爲隊列實現,新增修改要多修改節點的pre、next指針,且地址非連續,尋址時也會比array花更多時間。

參考

此次,完全弄懂接口及抽象類
Jdk1.6 Collections Framework源碼解析(3)-ArrayDeque

相關文章
相關標籤/搜索