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

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

父類和接口

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

父類

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

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

接口

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

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

框架設計

相繼看了接口和父類,樓主你到底想表達啥?嘿嘿,別急,我再反問一個經典問題——抽象類和接口有什麼區別?
你可能會有各種回答,比如抽象類能自己有自己的實現之類的。不能說不對,但這種答案相當於迷惑於奇技淫巧當中,未得正統。以設計角度來看,其實是is-a(抽象類)和has-a(接口)的區別!

  • 抽象類相當於某一個種族的基石

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

  • 接口則關注各種功能

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

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

這種設計不僅僅限於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是add的相反操作,原理相似,不多做分析……

參考

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章