Java 集合 ArrayList 源代碼分析(帶着問題看源碼)

今天學習下ArrayList的源代碼,不一樣於其餘人寫的博客,不少都是翻譯源代碼中的註釋,而後直接貼到文章中去。小編打算換一種書寫風格,帶着問題看源碼可能收穫會更大,本文將圍繞着下面幾個問題展開討論。java

1、問題產生

  • 一、爲何ArrayList集合中存儲元素的容器聲明爲transient Object[] elementData;程序員

  • 二、既然ArrayList能夠自動擴容,那麼它的擴容機制是怎樣實現的?數組

  • 三、調用ArrayListiterator()返回的迭代器是怎樣的?bash

  • 四、採用ArrayList的迭代器遍歷集合時,對集合執行相關修改操做時爲何會拋出ConcurrentModificationException,咱們該如何避免?運維

  • 五、當集合擴容或者克隆時免不了對集合進行拷貝操做,那麼ArrayList的數組拷貝是怎麼實現的?oop

  • 六、ArrayList中的序列化機制post

小編對ArrayList源碼大概瀏覽了以後,總結出以上幾個問題,帶着這些問題,讓咱們一塊兒翻開源碼解決吧!學習

2、問題解答

一、爲何ArrayList集合中存儲元素的容器聲明爲transient Object[] elementData;

ArrayList是一個集合容器,既然是一個容器,那麼確定須要存儲某些東西,既然須要存儲某些東西,那總得有一個存儲的地方吧!就比如說你須要裝一噸的水,總得有個池子給你裝吧!或者說你想裝幾十毫升水,總得那個瓶子或者袋子給你裝吧!區別就在於不一樣大小的水,咱們須要的容器大小也不相同而已!ui

既然ArrayList已經支持泛型了,那麼爲何ArrayList源碼的容器定義爲何還要定義成下面的Object[]類型呢?this

transient Object[] elementData;

其實不管你採用transient E[] elementData;的方式聲明,或者是採用transient Object[] elementData;聲明,都是容許的,差異在於前者要求咱們咱們在具體實例化elementData時須要作一次類型轉換,而此次類型轉換要求咱們程序員保證這種轉換不會出現任何錯誤。爲了提醒程序員關注可能出現的類型轉換異常,編譯器會發出一個Type safety: Unchecked cast from String[] to E[]警告,這樣講不知道會不會很懵比,下面的代碼告訴你:

public class MyList<E> {
    // 聲明數組,類型爲E[]
    E[] DATAS;
    // 初始化數組,必須作一次類型轉換
    public MyList(int initialCapacity) {
    	DATAS = (E[]) new Object[initialCapacity];
    }
    public E getDATAS(int index) {
    	return DATAS[index];
    }
    public void setDATAS(E[] dATAS) {
    	DATAS = dATAS;
    }
}
複製代碼

上面的代碼在1處咱們聲明瞭E[]數組,具體類型取決於你傳入E的實際類型,可是要注意,當你對DATAS進行初始化時,你不能像下面這樣初始化:

E[] DATAS = new E[10]; // 這句代碼將報錯

也就是說,泛型數組是不能具體化的,也就是不能經過new 泛型[size];的方式進行具體化,那麼怎麼解決呢?有兩種方式:

  • 一、進行前面說的作一次轉換,但不推薦

    就像上面代碼所展現的,咱們能夠初始化成Object[]類型以後再轉換成E[],但前提是你得保證此次轉換不會出現任何錯誤,一般咱們不建議這樣子寫!

  • 二、直接聲明爲Object[]

    這種方式也是ArrayList源碼的定義方式,那麼咱們來看看ArrayList是怎麼初始化的:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 此處直接new Object[],不會出現任何錯誤
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
複製代碼

可是有一點還須要注意,但你調用ArrayListtoArray方法將集合轉換爲對象數組時,有可能出現意想不到的結果,具體可參考小編的另一篇博文。

[ArrayList 其實也有雙胞胎,但區別仍是挺大的!]

總結: 總的來講,咱們要知道泛型數組是不能具體化的,以及其解決辦法!你可能會很好奇我爲何沒有講transient,這個小編放到下面序列化反序列化時講。

二、既然ArrayList能夠自動擴容,那麼它的擴容機制是怎樣實現的?

有時候,咱們得保證當增長水的時,原來的容器也能夠裝入新的的水而不至於溢出,也就是ArrayList的自動擴容機制。咱們能夠想象,假如列表大小爲10,那麼正常狀況下只能裝10個元素,咱們很好奇在此以後調用add()方法時底層作了什麼神奇的事,因此咱們看看add()方法是怎麼實現的:

// 增長一個元素
public boolean add(E e) {
    // 確保內部容量大小,size指的是當前列表的實際元素個數
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}
複製代碼

從上面方法能夠看出先判斷內部容量是否足夠知足size + 1個元素,若是能夠,就直接elementData[size++] = e;,不然就須要擴容,那麼怎麼擴容呢?咱們到ensureCapacityInternal()方法看看,這裏有一點很重要,請記住下面的參數:

  • minCapacity永遠表明增長以後實際的總元素個數
  • newCapacity永遠表示列表可以知足存儲minCapacity個元素列表所須要擴容的大小
// 校驗內部容量大小
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 這個方法只有首次調用時會用到,否則默認返回 minCapacity
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 這裏若是成立,表示該ArrayList是剛剛初始化,尚未add進任何元素
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
// 擴容判斷
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 判斷是否須要擴容,elementData.length表示列表的空間總大小,不是列表的實際元素個數,size纔是列表的實際元素個數
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
複製代碼

上面會判斷集合是否剛剛初始化,即尚未調用過add()方法,若是成立,則將集合默認擴容至10,DEFAULT_CAPACITY的值爲10,取最大值。最後一個方法的grow()成立的條件是容器的元素大於10且沒有可用空間,即須要擴容了,咱們再看看grow()方法:

private void grow(int minCapacity) {
    // 獲取舊的列表大小
    int oldCapacity = elementData.length;
    // 擴容以後的新的容器大小,默認增長一半 ..............................1
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 若是擴容一半以後還不足,則新的容器大小等於minCapacity.............................2
    if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
    // 若是新的容器大小比MAX_ARRAY_SIZE還大,
    if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);
    // 數組拷貝操做
    elementData = Arrays.copyOf(elementData, newCapacity);
}
// 最大不能超過Integer.MAX_VALUE
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
    	throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
複製代碼

上面1>>表示右移,也就是至關於除以2,減爲一半,2處可能調用addAll()方法時成立。

下面咱們列舉幾種狀況:

ID 狀況描述 調用add()? 調用addAll(size)? + size大小 執行結果
1 列表剛初始化 初始化一個長度爲10的列表,即容器擴容至10個單位
2 列表實際元素個數爲10,實際大小也爲10,此時調用add操做 容器擴容至15,容器元素個數爲11,即有4個位置空閒
3 列表實際元素個數爲10,列表長度也爲10,此時調用addAll操做 是 + 5 容器擴容至15,沒有空餘
4 列表實際元素個數爲5,列表長度爲10,此時調用addAll()操做 是 + 10 容器擴容至15,沒有空餘

總結:

擴容機制以下:

  • 一、先默認將列表大小newCapacity增長原來一半,即若是原來是10,則新的大小爲15;
  • 二、若是新的大小newCapacity依舊不能知足add進來的元素總個數minCapacity,則將列表大小改成和minCapacity同樣大;即若是擴大一半後newCapacity爲15,但add進來的總元素個數minCapacity爲20,則15明顯不能存儲20個元素,那麼此時就將newCapacity大小擴大到20,剛恰好存儲20個元素;
  • 三、若是擴容後的列表大小大於2147483639,也就是說大於Integer.MAX_VALUE - 8,此時就要作額外處理了,由於實際總元素大小有可能比Integer.MAX_VALUE還要大,當實際總元素大小minCapacity的值大於Integer.MAX_VALUE,即大於2147483647時,此時minCapacity的值將變爲負數,由於int是有符號的,當超過最大值時就變爲負數

小編認爲,上面第3點也體現了一種智慧,即當同樣東西有可能出錯時,咱們應該提早對其作處理,而不要等到錯誤發生時再對其進行處理。也就是咱們運維要作監控的目的。

三、調用ArrayListiterator()返回的迭代器是怎樣的?

咱們都知道全部集合都是Collection接口的實現類,又由於Collection繼承了Iterable接口,所以全部集合都是可迭代的。咱們經常會採用集合的迭代器來遍歷集合元素,就像下面的代碼:

ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
// 獲取集合的迭代器對象
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String item = iter.next();
    System.err.println(item);
}
複製代碼

咱們能夠經過調用集合的iterator()方法獲取集合的迭代器對象,那麼在ArrayList中,iterator()方法是怎麼實現的呢?

public Iterator<E> iterator() {
    return new Itr();
}
複製代碼

超級簡單,原來是新建了一個叫Itr的對象那麼這個Itr又是什麼呢?打開源碼咱們發現Itr類實際上是ArrayList的一個內部類,定義以下:

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;......................... 1
    Itr() {}
    public boolean hasNext() {...}// 具體實現被我刪除了
    public E next() {...}
    public void remove() {...}
    public void forEachRemaining(Consumer<? super E> consumer) {...}
    final void checkForComodification() {...}
}
複製代碼

該迭代器實現了Iterator接口並實現了相關方法,提供咱們對集合的遍歷能力。總結:ArrayList的迭代器默認是其內部類實現,實現一個自定義迭代器只須要實現Iterator接口並實現相關方法便可。而實現Iterable接口表示該實現類具備像for-each loop迭代遍歷的能力。

四、採用ArrayList的迭代器遍歷集合時,對集合執行相關修改操做時爲何會拋出ConcurrentModificationException,咱們該如何避免?

上面第3小節咱們查看了ArrayList迭代器的源代碼,咱們都知道,若是在迭代的過程當中調用非迭代器內部的remove或者clear方法將會拋出ConcurrentModificationException異常,那究竟是爲何呢?咱們一塊兒來看看。首先這裏設計兩個很重要的變量,一個是expectedModCount,另外一個是modCount,expectedModCount在集合內部迭代器中定義,就像上面第三小節源碼1處所示,modCountAbstractList中定義。就像第三小節1處所看到的,默認二者是相等的,即expectedModCount = modCount,只有當其不想等的狀況下就會拋出異常。真的是不想等就拋異常嗎?咱們來看看迭代器內部的next方法:

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];
}
// 具體檢查
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
複製代碼

能夠看出確實是當它們二者之間不想等時就報錯,問題來了,那麼何時會致使它們不想等呢?不急,咱們來看看ArrayListremove方法:

public E remove(int index) {
    rangeCheck(index);
    // 這裏會修改modCount的值
    modCount++;
    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
    
    return oldValue;
}
複製代碼

能夠看出當調用remove()方法時確實是修改了modCount的值,致使報錯。那咱們怎麼作才能不報錯有想在迭代過程當中增長或者刪除數據呢?答案是使用迭代器內部的remove()方法。

總結:

迭代器迭代集合時不能對被迭代集合進行修改,緣由是modCountexpectedModCount兩個變量值不想等致使的!

五、當集合擴容或者克隆時免不了對集合進行拷貝操做,那麼ArrayList的數組拷貝是怎麼實現的?

ArrayList中對集合的拷貝是經過調用ArrayscopyOf方法實現的,具體以下:

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());.................2
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    // 在建立新數組對象以前會先對傳入的數據類型進行斷定
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}
複製代碼

最後還調用了Systemarraycopy方法。

六、ArrayList中的序列化機制

第一小節咱們知道ArrayList存儲數據的定義方式爲:

transient Object[] elementData;
複製代碼

咱們會以爲很是奇怪,這是一個集合存儲元素的核心,卻聲明爲transient,是否是就說就不序列化了?這不科學呀!其實集合存儲的數據仍是會序列化的,具體咱們看看ArrayList中的writeObject方法:

writeObject

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();
    
    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);
    
    // 這個地方作一個序列化操做
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
複製代碼

從上面的代碼中咱們能夠看出ArrayList實際上是有對elementData進行序列化的,只不過這樣作的緣由是由於elementData中可能會有不少的null元素,爲了避免把null元素也序列化出去,因此自定義了writeObjectreadObject方法。

謝謝閱讀,歡迎評論,共同探討~

相關文章
相關標籤/搜索