天天看點

集合源碼分析之一:ArrayList源碼解析(JDK8)

目錄

概要

構造方法

常用API

1 增

2 删

3 改

5 清空,clear

6 包含 contain

7 判空 isEmpty()

8 疊代器 Iterator.

9 System.arraycopy()和Arrays.copyOf()方法

10 為什麼數組長度的最大值是Integer.MAX_VALUE - 8

11 modCount作用

總結

概要

概括的說,

ArrayList

 是一個動态數組,它是線程不安全的,允許元素為null。 

其底層資料結構依然是數組,它實作了

List<E>, RandomAccess, Cloneable, java.io.Serializable

接口,其中

RandomAccess

代表了其擁有随機快速通路的能力,

ArrayList

可以以O(1)的時間複雜度去根據下标通路元素。

因其底層資料結構是數組,是以可想而知,它是占據一塊連續的記憶體空間(容量就是數組的

length

),是以它也有數組的缺點,空間效率不高。

由于數組的記憶體連續,可以根據下标以O1的時間讀寫(改查)元素,是以時間效率很高。

當集合中的元素超出這個容量,便會進行擴容操作。擴容操作也是

ArrayList

 的一個性能消耗比較大的地方,是以若我們可以提前預知資料的規模,應該通過

public ArrayList(int initialCapacity) {}

構造方法,指定集合的大小,去建構

ArrayList

執行個體,以減少擴容次數,提高效率。

或者在需要擴容的時候,手動調用

public void ensureCapacity(int minCapacity) {}

方法擴容。 

不過該方法是

ArrayList

的API,不是

List

接口裡的,是以使用時需要強轉: 

((ArrayList)list).ensureCapacity(30);

當每次修改結構時,增加導緻擴容,或者删,都會修改modCount。

構造方法

//預設構造函數裡的空數組
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //存儲集合元素的底層實作:真正存放元素的數組
    transient Object[] elementData; // non-private to simplify nested class access
    //目前元素數量
    private int size;

    //預設構造方法
    public ArrayList() {
        //預設構造方法隻是簡單的将 空數組指派給了elementData
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    //空數組
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //帶初始容量的構造方法
    public ArrayList(int initialCapacity) {
        //如果初始容量大于0,則建立一個長度為initialCapacity的Object數組.
        //注意這裡并沒有修改size(對比第三個構造函數)
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//如果容量為0,直接将EMPTY_ELEMENTDATA指派給elementData
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//容量小于0,直接抛出異常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    //利用别的集合類來建構ArrayList的構造函數
    public ArrayList(Collection<? extends E> c) {
        //直接利用Collection.toArray()方法得到一個對象數組,并指派給elementData 
        elementData = c.toArray();
        //因為size代表的是集合元素數量,是以通過别的集合來構造ArrayList時,要給size指派
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)//這裡是當c.toArray出錯,沒有傳回Object[]時,利用Arrays.copyOf 來複制集合c中的元素到elementData數組中
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //如果集合c元素數量為0,則将空數組EMPTY_ELEMENTDATA指派給elementData 
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
           

小結一下,構造函數走完之後,會建構出數組elementData和數量size。

這裡大家要注意一下

Collection.toArray()

這個方法,在Collection子類各大集合的源碼中,高頻使用了這個方法去獲得某Collection的所有元素。

關于方法:

Arrays.copyOf(elementData, size, Object[].class)

,就是根據class的類型來決定是new 還是反射去構造一個泛型數組,同時利用native函數,批量指派元素至新數組中。 

如下:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        //根據class的類型來決定是new 還是反射去構造一個泛型數組
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        //利用native函數,批量指派元素至新數組中。
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
           

常用API

1 增

每次 add之前,都會判斷add後的容量,是否需要擴容。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;//在數組末尾追加一個元素,并修改size
    return true;
}
    private static final int DEFAULT_CAPACITY = 10;//預設擴容容量 10
    private void ensureCapacityInternal(int minCapacity) {
        //利用 == 可以判斷數組是否是用預設構造函數初始化的
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }


private void ensureExplicitCapacity(int minCapacity) {
    modCount++;//如果确定要擴容,會修改modCount 

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

//需要擴容的話,預設擴容一半
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);//預設擴容一半
    if (newCapacity - minCapacity < 0)//如果還不夠 ,那麼就用 能容納的最小的數量。(add後的容量)
        newCapacity = minCapacity;
    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);//拷貝,擴容,建構一個新數組,
}
           
public void add(int index, E element) {
    rangeCheckForAdd(index);//越界判斷 如果越界抛異常

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index); //将index開始的資料 向後移動一位
    elementData[index] = element;
    size++;
}
           
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount //确認是否需要擴容
    System.arraycopy(a, 0, elementData, size, numNew);// 複制數組完成複制
    size += numNew;
    return numNew != 0;
}
           
public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);//越界判斷

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount //确認是否需要擴容

    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);//移動(複制)數組

    System.arraycopy(a, 0, elementData, index, numNew);//複制數組完成批量指派
    size += numNew;
    return numNew != 0;
}
           

總結: 

add、addAll。 

先判斷是否越界,是否需要擴容。 

如果擴容, 就複制數組。 

然後設定對應下标元素值。

值得注意的是: 

1 如果需要擴容的話,預設擴容一半。如果擴容一半不夠,就用目标的size作為擴容後的容量。 

2 在擴容成功後,會修改modCount

2 删

public E remove(int index) {
    rangeCheck(index);//判斷是否越界
    modCount++;//修改modeCount 因為結構改變了
    E oldValue = elementData(index);//讀出要删除的值
    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  //置空原尾部資料 不再強引用, 可以GC掉
    return oldValue;
}
    //根據下标從數組取值 并強轉
    E elementData(int index) {
        return (E) elementData[index];
    }

//删除該元素在數組中第一次出現的位置上的資料。 如果有該元素傳回true,如果false。
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);//根據index删除元素
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
//不會越界 不用判斷 ,也不需要取出該元素。
private void fastRemove(int index) {
    modCount++;//修改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  //置空 不再強引用
}

//批量删除
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);//判空
    return batchRemove(c, false);
}
//批量移動
private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;//w 代表批量删除後 數組還剩多少元素
    boolean modified = false;
    try {
        //高效的儲存兩個集合公有元素的算法
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement) // 如果 c裡不包含目前下标元素, 
                elementData[w++] = elementData[r];//則保留
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        if (r != size) { //出現異常會導緻 r !=size , 則将出現異常處後面的資料全部複制覆寫到數組裡。
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;//修改 w數量
        }
        if (w != size) {//置空數組後面的元素
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;//修改modCount
            size = w;// 修改size
            modified = true;
        }
    }
    return modified;
}
           

從這裡我們也可以看出,當用來作為删除元素的集合裡的元素多餘被删除集合時,也沒事,隻會删除它們共同擁有的元素。

小結: 

1 删除操作一定會修改modCount,且可能涉及到數組的複制,相對低效。 

2 批量删除中,涉及高效的儲存兩個集合公有元素的算法,可以留意一下。

3 改

不會修改modCount,相對增删是高效的操作。

public E set(int index, E element) {
    rangeCheck(index);//越界檢查
    E oldValue = elementData(index); //取出元素 
    elementData[index] = element;//覆寫元素
    return oldValue;//傳回元素
}
           

不會修改modCount,相對增删是高效的操作。

public E get(int index) {
    rangeCheck(index);//越界檢查
    return elementData(index); //下标取資料
}
E elementData(int index) {
    return (E) elementData[index];
}
           

5 清空,clear

會修改modCount。

public void clear() {
    modCount++;//修改modCount
    // clear to let GC do its work
    for (int i = 0; i < size; i++)  //将所有元素置null
        elementData[i] = null;

    size = 0; //修改size 
}
           

6 包含 contain

//普通的for循環尋找值,隻不過會根據目标對象是否為null分别循環查找。
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}
//普通的for循環尋找值,隻不過會根據目标對象是否為null分别循環查找。
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
           

7 判空 isEmpty()

public boolean isEmpty() {
    return size == 0;
}
           

8 疊代器 Iterator.

public Iterator<E> iterator() {
    return new Itr();
}
/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return //預設是0
    int lastRet = -1; // index of last element returned; -1 if no such  //上一次傳回的元素 (删除的标志位)
    int expectedModCount = modCount; //用于判斷集合是否修改過結構的 标志

    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)//再次判斷是否越界,在 我們這裡的操作時,有異步線程修改了List
            throw new ConcurrentModificationException();
        cursor = i + 1;//遊标+1
        return (E) elementData[lastRet = i];//傳回元素 ,并設定上一次傳回的元素的下标
    }

    public void remove() {//remove 掉 上一次next的元素
        if (lastRet < 0)//先判斷是否next過
            throw new IllegalStateException();
        checkForComodification();//判斷是否修改過

        try {
            ArrayList.this.remove(lastRet);//删除元素 remove方法内會修改 modCount 是以後面要更新Iterator裡的這個标志值
            cursor = lastRet; //要删除的遊标
            lastRet = -1; //不能重複删除 是以修改删除的标志位
            expectedModCount = modCount;//更新 判斷集合是否修改的标志,
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
//判斷是否修改過了List的結構,如果有修改,抛出異常
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
           

9 System.arraycopy()和Arrays.copyOf()方法

  通過上面源碼我們發現這兩個實作數組複制的方法被廣泛使用而且很多地方都特别巧妙。比如下面add(int index, E element)方法就很巧妙的用到了arraycopy()方法讓數組自己複制自己實作讓index開始之後的所有成員後移一個位置:

/**
     * 在此清單中的指定位置插入指定的元素。 
     *先調用 rangeCheckForAdd 對index進行界限檢查;然後調用 ensureCapacityInternal 方法保證capacity足夠大;
     *再将從index開始之後的所有成員後移一個位置;将element插入index位置;最後size加1。
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //arraycopy()方法實作數組自己複制自己
        //elementData:源數組;index:源數組中的起始位置;elementData:目标數組;index + 1:目标數組中的起始位置; size - index:要複制的數組元素的數量;
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }
           

又如toArray()方法中用到了copyOf()方法

/**
     *以正确的順序(從第一個到最後一個元素)傳回一個包含此清單中所有元素的數組。 
     *傳回的數組将是“安全的”,因為該清單不保留對它的引用。 (換句話說,這個方法必須配置設定一個新的數組)。
     *是以,調用者可以自由地修改傳回的數組。 此方法充當基于陣列和基于集合的API之間的橋梁。
     */
    public Object[] toArray() {
    //elementData:要複制的數組;size:要複制的長度
        return Arrays.copyOf(elementData, size);
    }
           

兩者聯系與差別

聯系: 看兩者源代碼可以發現

copyOf()

内部調用了

System.arraycopy()

方法 差別:

  1. arraycopy()需要目标數組,将原數組拷貝到你自己定義的數組裡,而且可以選擇拷貝的起點和長度以及放入新數組中的位置
  2. copyOf()是系統自動在内部建立一個數組,并傳回該數組。

10 為什麼數組長度的最大值是Integer.MAX_VALUE - 8

數組作為一個對象,需要一定的記憶體存儲對象頭資訊,對象頭資訊最大占用記憶體不可超過8位元組。

數組的對象頭資訊相較于其他Object,多了一個表示數組長度的資訊。

具體的對象頭内容,可以參考 JVM分析

11 modCount作用

protected transient int modCount = 0;

private int expectedModCount = modCount;

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

該字段表示list結構上被修改的次數。結構上的修改指的是那些改變了list的長度大小或者使得周遊過程中産生不正确的結果的其它方式。

該字段被Iterator以及ListIterator的實作類所使用,如果該值被意外更改,Iterator或者ListIterator 将抛出ConcurrentModificationException異常,

這是jdk在面對疊代周遊的時候為了避免不确定性而采取的快速失敗原則。

子類對此字段的使用是可選的,如果子類希望支援快速失敗,隻需要覆寫該字段相關的所有方法即可。單線程調用不能添加删除terator正在周遊的對象,

否則将可能抛出ConcurrentModificationException異常,如果子類不希望支援快速失敗,該字段可以直接忽略。

總結

  1. 增删改查中, 增導緻擴容,則會修改modCount,删一定會修改。 改和查一定不會修改modCount。
  2. 擴容操作會導緻數組複制,批量删除會導緻 找出兩個集合的交集,以及數組複制操作,是以,增、删都相對低效。 而 改、查都是很高效的操作。
  3. 是以,結合特點,在使用中,以Android中最常用的展示清單為例,清單滑動時需要展示每一個Item(element)的數組,是以 查 操作是最高頻的。相對來說,增操作 隻有在清單加載更多時才會用到 ,而且是在清單尾部插入,是以也不需要移動資料的操作。而删操作則更低頻。 故選用ArrayList作為儲存資料的結構。
  4. 在面試中還有可能會問到和

    Vector

    的差別,我大緻看了一下

    Vector

    的源碼,内部也是數組做的,差別在于

    Vector

    在API上都加了

    synchronized

    是以它是線程安全的,以及

    Vector

    擴容時,是翻倍size,而

    ArrayList

    是擴容50%。