簡明數(shù)據(jù)結構源碼閱讀(一)-- ArrayList

推薦閱讀時間 13min+

目錄

  1. ArrayList 關鍵詞
  2. 源碼閱讀
  3. 問題解答和總結

前言

本文基于Java8的源代碼進行了源代碼的代碼閱讀分析巨缘,關于Java8中的新增加的Stream API特性在數(shù)據(jù)結構中的時候會在之后之后專門使用一篇文章的內(nèi)容進行介紹寞蚌,這里只介紹源代碼和代碼邏輯分析读存。

ArrayList關鍵詞

閱讀ArrayList的相關文檔润绎,很容易從中提取出如下的關鍵詞:

  1. backing array
  2. capacity / incremental reallocation
  3. structural modification / fail-fast

綜合這三個關鍵詞峻凫,我們不難了解到ArrayList的特性和問題炊汹。backing array 指出實現(xiàn)了ArrayList的幕后機制的是一個幕后數(shù)組(backing array篡撵,Object數(shù)組)褐着,其中它的容量(capacity)是可以增量調(diào)整的(incremental reallocation)坷澡,并且ArrayList并不像它的前輩Vector是一種線程安全的容器,如果出現(xiàn)了結構性變化(structural modification含蓉,比如 add remove频敛,set不是 ,會通過modCount標記這個結構性變化)會使用一種機制馅扣,這種機制不會讓存在這缺陷的過程繼續(xù)下去斟赚,而是立刻停止系統(tǒng)工作,這種機制也被稱為fail-fast差油。fail-fast是一種盡最大努力(best-effort)的機制拗军,不可以基于它拋出的異常做錯誤控制任洞。

從三個關鍵詞中衍生出三個問題

  1. 如果是一個Object數(shù)組,ArrayList如何實現(xiàn)類型檢查和相關的問題发侵?
  2. fail-fast機制如何實現(xiàn)的交掏,具體如何體現(xiàn)?
  3. ArrayList如何實現(xiàn)擴容的刃鳄?

關于這三個問題盅弛,希望讀者能在我的分析中思考,最后我也會給出答案叔锐。

源碼分析

ArrayList的繼承關系圖


其中ArrayListSpliterator和Strem API有關挪鹏,這里后續(xù)會詳細分析。

ArrayList中的關鍵常量

這些常量都是見名知意的愉烙,{}表示空數(shù)組字面量

名稱 類型 初始值 意義
DEFAULT_CAPACITY Object[] 10 表示下面的DEFAULT空數(shù)組的大小
DEFAULTCAPACITY_EMPTY_ELEMENTDATA Object[] {}
elementData Object[] {}
EMPTY_ELEMENTDATA Object[] {} 空數(shù)組大小為0讨盒,和DEFAULT數(shù)組相區(qū)別
size int 0 List大小
modCount int 0 fail-fast標記
MAX_ARRAY_SIZE int Integer.MAX_VALUE-8 最大大小

構造函數(shù)

ArrayList的中的三個構造函數(shù)

  1. 空構造函數(shù)
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

直接初始化了elementDataDEFAULT數(shù)組,大小默認為10

  1. 帶有初始大小參數(shù)的構造函數(shù)
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
  1. 將現(xiàn)有的Collection的內(nèi)容初始化ArrayList
 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652) ****
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

其中Collection中的元素的在ArrayList中的順序和Collection citerator的遍歷順序相同齿梁,具體的實現(xiàn)在toArray函數(shù)中催植,這里采取ArrayListLinkedListtoArray作為參考:

  1. ArrayList
 public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
  1. LinkedList
 public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }

ArrayList中迭代器迭代的順序是按照數(shù)組的順序,LinkedList中的順序是按照Node連接的順序勺择,沒毛病创南。
這里看一下 打****的問題,這是一個官方bug省核,為什么toArray()不一定能返回Object[]? 事實上這是一個類型轉(zhuǎn)換的問題稿辙,這篇文章說的非常清晰,輕容許我簡要說明一下他的觀點:

 public class MyList<E> extends ArrayList<E> {

        // toArray() 的同名方法
        public String[] toArray() {
            return new String[]{"1", "2", "3"};
        }

    }

因為方法的重載不看返回值的气忠,如果子類定義了這個方法邻储,當調(diào)用toArray()的時候,就回返回String[]旧噪,這樣就會出現(xiàn)錯誤吨娜。

add相關操作

add有很多相關方法,不一一列舉代碼了淘钟,具體實現(xiàn)大同小異宦赠。add操作的代碼調(diào)用情況圖如下:


這些方法都依賴ensureCapacity相關方法,這里要著重分析一下米母。

ensure相關方法

ensure相關方法都和擴容操作有關勾扭,minCapacity就是如果elementData容量不夠大,就會最小擴容到這個大小铁瞒,并且留意modCount的變化

 private void ensureCapacityInternal(int minCapacity) {
        //封裝方法 詳細調(diào)用妙色,直接看后面
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

  private static int calculateCapacity(Object[] elementData, int minCapacity) {
      //如果backing array是Default數(shù)組
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
          //體現(xiàn)了Default數(shù)組和其Default大小的對應關系
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

 private void ensureExplicitCapacity(int minCapacity) {
       //這是一個結構性變化,
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
    //首先嘗試擴容到原來的大小的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //下面的的兩個if語句指向了擴容情況的兩個極端:
        //不夠minCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //但是卻又大于最大數(shù)組大小
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

所以我們再看看最基本的add(E e)方法干了什么,看一個方法锥惋,其余的同名方法的做法大同小異:

public boolean add(E e) {
        //minCapacity = size + 1 然后在進行是否擴容的試探
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

綜上所述,add操作都是結構性變化的操作饲握,通過ensure相關函數(shù)進行擴容和modCount的自增(fail-fast防止多線程操作)栅表。再擴容期間笋鄙,每次都會擴容1.5倍,所以在感覺可能數(shù)據(jù)很多的情況下怪瓶,使用默認的無參數(shù)構造函數(shù)的所產(chǎn)生的10個空間是不夠的萧落。

remove相關操作


remove操作也是一個結構性變化的操作,我們主要看看幾個修改modCount的操作:
fastRemove

 public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
//fastRemove 之所以fast是因為不需要在這個方法中做邊界判斷洗贰,邊界判斷在上面的for循環(huán)已經(jīng)完成了
 private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

batchRemove()

 private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        //使用了一種類似于雙指針的操作
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
            //如果需要保留c中的元素找岖,complement取true
                if (c.contains(elementData[r]) == complement)
                  //利用雙指針進行復制覆蓋原來的位置
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            //因為ArrayList是可以容納null元素的,所以contains不會拋出異常敛滋,但是有些容器不能容納null的時候许布,會從上面的if語句進入finally塊
          //直接默認r后面的是需要保留的
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
            //set不是結構性變化操作,刪除才是绎晃,所以是size - w
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

介紹完了modCount的兩個主要修改的地方蜜唾,下面請看modCount對于fail-fast的作用的體現(xiàn)。

checkForComodification()

ArrayList有兩個迭代器ItrListItr庶艾,ListItr在上面的圖中可以看出是繼承了Itr的袁余。這篇優(yōu)秀的文章
中給我們展示了CME(ConcurrentModificationException)的兩種出現(xiàn)的的情況,不難看出咱揍,CME常常出現(xiàn)在使用迭代器迭代的情況下颖榜,或者Java的語法糖foreach中,對List進行結構性修改煤裙。通過閱讀Itr的源碼可以對CME的出現(xiàn)原因了解的非常清楚掩完。


Itr中的重要常量:

名稱 類型 初始值 意義
cursor int 0 游標
lastRet int -1
expectedModCount int modCount 在初始化Itr時會復制modCount,用于確保不會有其他線程對其進行修改

Itrnextremove方法中都需要調(diào)用checkForComodification方法:

     public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

在上面的文章中列舉的單線程情況中硼砰,因為ArrayListremove方法雖然修改modCount但是且蓬,和Itr中的expectedModCount不符,而導致異常题翰。說穿了恶阴,這實際上是因為外部類的modCount和內(nèi)部類的expectedModCount不相同導致的問題,這只能說是一個缺陷遍愿。在多線程的環(huán)境下存淫,使用modCount才是比較好的控制策略耘斩。

SubList

SubListArrayList的一個內(nèi)部類沼填,其中的方法大體上和ArrayList一致,可以看做是對ArrayList的一個“視圖”括授,但是這個視圖是可以影響其原本映射的List的坞笙。

  public E set(int index, E e) {
            rangeCheck(index);
            checkForComodification();
            E oldValue = ArrayList.this.elementData(offset + index);
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }

        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }

問題解答和總結

雖然這篇文章并沒有對于整體的所有代碼進行逐行解釋岩饼,但是大體上能夠讓讀者對于ArrayList源碼有一個直觀地認識。
下面來回答文章開頭提的幾個問題:

  1. fail-fast是通過modCountItr還有ListItr薛夜,SubList等中的checkForComodification操作實現(xiàn)的籍茧。但是在foreach和while中使用迭代器模式進行遍歷時,禁止使用ArrayListremoveadd操作梯澜。這樣的機制在最大程度上避免了多線程修改寞冯。
  2. 擴容機制是通過ensure函數(shù)實現(xiàn)的,如果大小不夠會通過擴充1.5倍看看晚伙。
  3. 類型檢查吮龄。首先使用了靜態(tài)類型檢查,泛型做了一定的限定咆疗。其次漓帚,針對toArray()這里天然的語法缺陷,也做了個邏輯層面的判斷午磁,規(guī)避類型不一產(chǎn)生的問題尝抖。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市迅皇,隨后出現(xiàn)的幾起案子昧辽,更是在濱河造成了極大的恐慌,老刑警劉巖喧半,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奴迅,死亡現(xiàn)場離奇詭異,居然都是意外死亡挺据,警方通過查閱死者的電腦和手機取具,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扁耐,“玉大人暇检,你說我怎么就攤上這事⊥癯疲” “怎么了块仆?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長王暗。 經(jīng)常有香客問我悔据,道長,這世上最難降的妖魔是什么俗壹? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任科汗,我火速辦了婚禮,結果婚禮上绷雏,老公的妹妹穿的比我還像新娘头滔。我一直安慰自己怖亭,他們只是感情好,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布坤检。 她就那樣靜靜地躺著兴猩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪早歇。 梳的紋絲不亂的頭發(fā)上倾芝,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音箭跳,去河邊找鬼蛀醉。 笑死,一個胖子當著我的面吹牛衅码,可吹牛的內(nèi)容都是我干的拯刁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼逝段,長吁一口氣:“原來是場噩夢啊……” “哼垛玻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奶躯,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帚桩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嘹黔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體账嚎,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年儡蔓,在試婚紗的時候發(fā)現(xiàn)自己被綠了郭蕉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡喂江,死狀恐怖召锈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情获询,我是刑警寧澤涨岁,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站吉嚣,受9級特大地震影響梢薪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜尝哆,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一秉撇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦畜疾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至奸焙,卻和暖如春瞎暑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背与帆。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工了赌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玄糟。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓勿她,卻偏偏與公主長得像,于是被迫代替她去往敵國和親阵翎。 傳聞我的和親對象是個殘疾皇子逢并,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359