較真兒學源碼系列-ArrayList(逐行源碼帶你分析做者思路)

Java版本:8u261。java


1 簡介

ArrayList做爲最基礎的集合類,其底層是使用一個動態數組來實現的,這裏「動態」的意思是能夠動態擴容(雖然ArrayList能夠動態擴容,可是不會動態縮容)。可是與HashMap不一樣的是,ArrayList使用的是*1.5的擴容策略,而HashMap使用的是*2的方式。還有一點與HashMap不一樣:ArrayList的默認初始容量爲10,而HashMap爲16。數組

有意思的一點是:在Java 7以前的版本中,ArrayList的無參構造器是在構造器階段完成的初始化;而從Java 7開始,改成了在add方法中完成初始化,也就是延遲初始化。在HashMap中也有一樣的設計思路。併發

另外,同HashMap同樣,若是要存入一個很大的數據量而且事先知道要存入的這個數據量的固定值時,就能夠往構造器裏傳入這個初始容量,以此來避免之後的頻繁擴容。大數據


2 構造器

/**
 * ArrayList:
 * 無參構造器
 */
public ArrayList() {
    //DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空實現「{}」,這裏也就是在作初始化
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 有參構造器
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        //initialCapacity>0就按照這個容量來初始化數組
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //EMPTY_ELEMENTDATA也是一個空實現「{}」,這裏也是在作初始化
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //若是initialCapacity爲負數,則拋出異常
        throw new IllegalArgumentException("Illegal Capacity: " +
                initialCapacity);
    }
}

3 add方法

3.1 add(E e)

添加指定的元素:this

/**
 * ArrayList:
 */
public boolean add(E e) {
    //查看是否須要擴容
    ensureCapacityInternal(size + 1);
    //size記錄的是當前元素的個數,這裏就直接往數組最後添加新的元素就好了,以後size再+1
    elementData[size++] = e;
    return true;
}

/**
 * 第6行代碼處:
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    /*
    minCapacity = size + 1

    以前說過,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空實現「{}」,這裏也就是在判斷是否是調用的無參構造器
    並第一次調用到此處
     */
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        /*
        若是是的話就返回DEFAULT_CAPACITY(10)和size+1之間的較大者。也就是說,數組的最小容量是10

        這裏有意思的一點是:調用new ArrayList<>()和new ArrayList<>(0)兩個構造器會有不一樣的默認容量(在HashMap中
        也是如此)。也就是說無參構造器的初始容量爲10,而傳進容量爲0的初始容量爲1。同時這也就是爲何會有
        EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA這兩個常量的存在,雖然它們的值都是「{}」
        緣由就在於無參構造器和有參構造器徹底就是兩種不一樣的實現策略:若是你想要具體的初始容量,那麼就調用有參構造器吧,
        即便傳入的是0也是符合這種狀況的;而若是你不在意初始的容量是多少,那麼就調用無參構造器就好了,這會給你默
        認爲10的初始容量
         */
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //若是調用的是有參構造器,或者調用無參構造器但不是第一次進來,就直接返回size+1
    return minCapacity;
}

/**
 * 第16行代碼處:
 */
private void ensureExplicitCapacity(int minCapacity) {
    //修改次數+1(快速失敗機制)
    modCount++;

    /*
    若是+1後指望的容量比實際數組的容量還大,就須要擴容了(若是minCapacity也就是size + 1後發生了數據溢出,
    那麼minCapacity就變爲了一個負數,而且是一個接近int最小值的數。而此時的elementData.length也會是一個接近
    int最大值的數,那麼該if條件也有可能知足,此時會進入到grow方法中的hugeCapacity方法中拋出溢出錯誤)
     */
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    //獲取擴容前的舊數組容量
    int oldCapacity = elementData.length;
    //這裏擴容後新數組的容量是採用舊數組容量*1.5的方式來實現的
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    /*
    若是新數組容量比+1後指望的容量還要小,此時把新數組容量修正爲+1後指望的容量(對應於newCapacity爲0或1的狀況)

    這裏以及後面的判斷使用的都是「if (a - b < 0)」形式,而不是常規的「if (a < b)」形式是有緣由的,
    緣由就在於須要考慮數據溢出的狀況:若是執行了*1.5的擴容策略後newCapacity發生了數據溢出,那麼它就同樣
    變爲了一個負數,而且是一個接近int最小值的數。而minCapacity此時也一定會是一個接近int最大值的數,
    那麼此時的「newCapacity - minCapacity」計算出來的結果就可能會是一個大於0的數。因而這個if條件
    就不會執行,而是會在下個條件中的hugeCapacity方法中處理這種溢出的問題。而若是這裏用的是
    「if (newCapacity < minCapacity)」,數據溢出的時候該if條件會返回true,因而newCapacity會
    錯誤地賦值爲minCapacity,而沒有使用*1.5的擴容策略
     */
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    /*
    若是擴容後的新數組容量比設定好的容量最大值(Integer.MAX_VALUE - 8)還要大,就從新設置一下新數組容量的上限

    同上面的分析,若是發生數據溢出的話,這裏的if條件可能也是知足的,那麼也會走進hugeCapacity方法中去處理
     */
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    /*
    能夠看到這裏是經過Arrays.copyOf(System.arraycopy)的方式來進行數組的拷貝,
    容量是擴容後的新容量newCapacity,將拷貝後的新數組賦值給elementData便可
     */
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 第83行代碼處:
 */
private static int hugeCapacity(int minCapacity) {
    //minCapacity對應於size+1,因此若是minCapacity<0就說明發生了數據溢出,就拋出錯誤
    if (minCapacity < 0)
        throw new OutOfMemoryError();
    /*
    若是minCapacity大於MAX_ARRAY_SIZE,就返回int的最大值,不然返回MAX_ARRAY_SIZE
    不論是哪一個,這都會將newCapacity從新修正爲一個大於0的數,也就是處理了數據溢出的狀況
    其實從這裏能夠看出:本方法中並無使用*1.5的擴容策略,只是設置了一個上限而已。可是在Java中
    真能申請獲得Integer.MAX_VALUE這麼大的數組空間嗎?其實不見得,這只是一個理論值。實際上須要考慮
    -Xms和-Xmx等一系列JVM參數所設置的值。因此這也可能就是MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
    其中-8的含義吧。無論如何,當數組容量達到這麼大的量級時,乘不乘1.5其實已經不過重要了)
     */
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

3.2 add(int index, E element)

在指定的位置處添加指定的元素:spa

/**
 * ArrayList:
 */
public void add(int index, E element) {
    //index參數校驗
    rangeCheckForAdd(index);

    //查看是否須要擴容
    ensureCapacityInternal(size + 1);
    /*
    這裏數組拷貝的意義,就是將index位置處以及後面的數組元素日後移動一位,以此來挪出一個位置
    System.arraycopy是直接對內存進行復制,在大數據量下,比for循環更快
     */
    System.arraycopy(elementData, index, elementData, index + 1,
            size - index);
    //而後將須要插入的元素插入到上面挪出的index位置處就能夠了
    elementData[index] = element;
    //最後size+1,表明添加了一個元素
    size++;
}

/**
 * 第6行代碼處:
 * 檢查傳入的index索引位是否越界,若是越界就拋異常
 */
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private String outOfBoundsMsg(int index) {
    return "Index: " + index + ", Size: " + size;
}

4 get方法

/**
 * ArrayList:
 */
public E get(int index) {
    //index參數校驗
    rangeCheck(index);

    return elementData(index);
}

/**
 * 第6行代碼處:
 * 這裏只檢查了index大於等於size的狀況,而index爲負數的狀況
 * 在elementData方法中會直接拋出ArrayIndexOutOfBoundsException
 */
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 第8行代碼處:
 * 能夠看到,這裏是直接從elementData數組中獲取指定index位置的數據
 */
@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

5 remove方法

5.1 remove(Object o)

刪除指定的元素:線程

/**
 * ArrayList:
 */
public boolean remove(Object o) {
    if (o == null) {
        //若是要刪除的元素爲null
        for (int index = 0; index < size; index++)
            //遍歷數組中的每個元素,找到第一個爲null的元素
            if (elementData[index] == null) {
                /*
                刪除這個元素,並返回true。這裏也就是在作清理的工做:遇到一個爲null的元素就清除掉
                注意這裏只會清除一次,並不會所有清除
                 */
                fastRemove(index);
                return true;
            }
    } else {
        //若是要刪除的元素不爲null
        for (int index = 0; index < size; index++)
            //找到和要刪除的元素是一致的數組元素
            if (o.equals(elementData[index])) {
                /*
                找到了一個就進行刪除,並返回true。注意這裏只會找到並刪除一個元素,
                若是要刪除全部的元素就調用removeAll方法便可
                 */
                fastRemove(index);
                return true;
            }
    }
    /*
    若是要刪除的元素爲null而且找不到爲null的元素,或者要刪除的元素不爲null而且找不到和要刪除元素相等的數組元素,
    就說明此時不須要刪除元素,直接返回false就好了
     */
    return false;
}

/**
 * 第14行和第26行代碼處:
 */
private void fastRemove(int index) {
    //修改次數+1
    modCount++;
    //numMoved記錄的是移動元素的個數
    int numMoved = size - index - 1;
    if (numMoved > 0)
        /*
        這裏數組拷貝的意義,就是將index+1位置處以及後面的數組元素往前移動一位,
        這會將index位置處的元素被覆蓋,也就是作了刪除
         */
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    /*
    由於上面是左移了一位,因此最後一個位置至關於騰空了,這裏也就是將最後一個位置(--size)置爲null
    固然若是上面計算出來的numMoved自己就小於等於0,也就是index大於等於size-1的時候(大於不太可能,
    是屬於異常的狀況),意味着不須要進行左移。此時也將最後一個位置置爲null就好了。置爲null以後,
    原有數據的引用就會被斷開,GC就能夠工做了
     */
    elementData[--size] = null;
}

5.2 remove(int index)

刪除指定位置處的元素:設計

/**
 * ArrayList:
 */
public E remove(int index) {
    //index參數校驗
    rangeCheck(index);

    //修改次數+1
    modCount++;
    //獲取指定index位置處的元素
    E oldValue = elementData(index);

    //numMoved記錄的是移動元素的個數
    int numMoved = size - index - 1;
    if (numMoved > 0)
        /*
        同上面fastRemove方法中的解釋,這裏一樣是將index+1位置處以及後面的數組元素往前移動一位,
        這會將index位置處的元素被覆蓋,也就是作了刪除(這裏是否能夠考慮封裝?)
         */
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    //同上,將最後一個位置(--size)置爲null
    elementData[--size] = null;

    //刪除以後,將舊值返回就好了
    return oldValue;
}

6 不要在foreach循環裏進行元素的remove/add操做

首先來看一下remove的狀況。正例:指針

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if ("2".equals(item)) {
        iterator.remove();
    }
}

反例:code

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

for (String item : list) {
    if ("2".equals(item)) {
        list.remove(item);
    }
}

運行上面的代碼能夠看到,使用迭代器的刪除操做是不會有問題、能成功刪除的;而使用foreach循環進行刪除則會拋出ConcurrentModificationException異常,但若是使用foreach循環刪除第一個元素「1」的時候又會發現不會拋出異常。那麼這究竟是爲何呢?

首先來看一下ArrayList中的內部類Itr,每次foreach遍歷都是經過它的hasNext和next方法來進行肯定的(普通的for循環不是經過這種方式,也就是說普通的for循環不會有這種問題):

/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    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];
    }

    //...
}

而拋出異常是在上面第17行代碼處的checkForComodification方法裏面拋出的,下面來看一下它的實現:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

能夠看到若是modCount和expectedModCount不等就會拋出ConcurrentModificationException異常。而上面說過,在add方法中的ensureExplicitCapacity方法中,會對modCount修改標誌位作+1的操做。這裏的modCount是爲了作快速失敗用的。快速失敗指的是若是在遇到併發修改時,迭代器會快速地拋出異常,而不是在未來某個不肯定的時間點冒着任意而又不肯定行爲的風險來進行操做,也就是將可能出現的bug點推前。在包括HashMap在內的不少集合類都是有快速失敗機制的。注意:這裏的併發修改指的並不都是發生在併發時的修改,也有多是在單線程中所作的修改致使的,就如同上面的反例同樣。

這裏拿上面的反例來舉例,ArrayList調用了兩次add方法,也就是此時的modCount應該爲2。而expectedModCount如上所示,一開始會初始化爲modCount的值,也就是也爲2。

第一次循環:

由於此時的modCount和expectedModCount都爲2,因此第一次循環中不會拋出異常,拋出異常都是發生在不是第一次循環的狀況中。而這也就是使用foreach循環刪除第一個元素「1」的時候不會拋出異常的緣由。在next方法走完後,foreach循環方法體中的remove方法的if條件判斷不知足,就結束了本次循環。

第二次循環:

第二次循環的hasNext和next方法都是能成功走完的,在這以後會進入到foreach循環方法體中的remove方法中,進行刪除元素。而此時的size-1變爲了1。上面分析過,在remove方法中的fastRemove方法中,會對modCount+1,也就變爲了3。

第三次循環:

而後會走入到第三次循環中的hasNext方法中。按照正常的狀況下該方法是會返回false的,但由於此時的size已經變爲了1,而此時的cursor爲2(cursor表明下一次的索引位置),因此二者不等,錯誤地返回了true,因此會繼續走入到next方法中的checkForComodification方法中,判斷此時的modCount和expectedModCount是否相等。由於此時的modCount已經變爲了3,和expectedModCount的值爲2不等,因此在此拋出了ConcurrentModificationException異常。同時這也就意味着,在foreach循環中作add操做也是會拋出異常的,由於add操做中也會修改modCount和size(具體拋出異常的過程這裏就再也不分析了,都是相似的)其實只要在foreach循環方法體中有進行修改modCount和size的操做,就都有多是會拋出異常的。

既然如今已經知道了foreach循環中使用remove/add操做拋出異常的緣由,那麼就能夠分析一下爲何使用迭代器進行相關操做就不會有問題呢?下面來分析一下上面正例的代碼,第5行代碼處的iterator方法以下:

public Iterator<E> iterator() {
    return new Itr();
}

能夠看到iterator方法也是返回了一個Itr內部類。而第6行和第7行代碼處的hasNext和next方法也就是Itr內部類中的hasNext和next方法,同時也就是上面分析過的方法,而區別在於第9行代碼處的remove操做。這裏的remove不是ArrayList中的remove操做,而是Itr內部類中的remove操做:

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

能夠看到第7行代碼處是調用了ArrayList的remove操做進行刪除的,但同時注意第10行代碼處會將expectedModCount更新爲此時modCount的最新值,這樣在next方法中就不會拋出異常了;在第8行代碼處會將cursor更新爲lastRet(lastRet表明上一次的索引位置),即將cursor-1(由於此時要remove,因此cursor指針須要減一)。這樣在hasNext方法中就會返回正確的值了。

相關文章
相關標籤/搜索