Java 集合 ArrayList 原始碼分析(帶著問題看原始碼)

擁抱心中的夢想發表於2018-06-22

今天學習下ArrayList的原始碼,不同於其他人寫的部落格,很多都是翻譯原始碼中的註釋,然後直接貼到文章中去。小編打算換一種書寫風格,帶著問題看原始碼可能收穫會更大,本文將圍繞著下面幾個問題展開討論。

一、問題產生

  • 1、為什麼ArrayList集合中儲存元素的容器宣告為transient Object[] elementData;

  • 2、既然ArrayList可以自動擴容,那麼它的擴容機制是怎樣實現的?

  • 3、呼叫ArrayListiterator()返回的迭代器是怎樣的?

  • 4、採用ArrayList的迭代器遍歷集合時,對集合執行相關修改操作時為什麼會丟擲ConcurrentModificationException,我們該如何避免?

  • 5、當集合擴容或者克隆時免不了對集合進行拷貝操作,那麼ArrayList的陣列拷貝是怎麼實現的?

  • 6、ArrayList中的序列化機制

小編對ArrayList原始碼大概瀏覽了之後,總結出以上幾個問題,帶著這些問題,讓我們一起翻開原始碼解決吧!

二、問題解答

1、為什麼ArrayList集合中儲存元素的容器宣告為transient Object[] elementData;

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

既然ArrayList已經支援泛型了,那麼為什麼ArrayList原始碼的容器定義為什麼還要定義成下面的Object[]型別呢?

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];的方式進行具體化,那麼怎麼解決呢?有兩種方式:

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

    就像上面程式碼所展示的,我們可以初始化成Object[]型別之後再轉換成E[],但前提是你得保證這次轉換不會出現任何錯誤,通常我們不建議這樣子寫!

  • 2、直接宣告為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,這個小編放到下面序列化反序列化時講。

2、既然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,沒有空餘

總結:

擴容機制如下:

  • 1、先預設將列表大小newCapacity增加原來一半,即如果原來是10,則新的大小為15;
  • 2、如果新的大小newCapacity依舊不能滿足add進來的元素總個數minCapacity,則將列表大小改為和minCapacity一樣大;即如果擴大一半後newCapacity為15,但add進來的總元素個數minCapacity為20,則15明顯不能儲存20個元素,那麼此時就將newCapacity大小擴大到20,剛剛好儲存20個元素;
  • 3、如果擴容後的列表大小大於2147483639,也就是說大於Integer.MAX_VALUE - 8,此時就要做額外處理了,因為實際總元素大小有可能比Integer.MAX_VALUE還要大,當實際總元素大小minCapacity的值大於Integer.MAX_VALUE,即大於2147483647時,此時minCapacity的值將變為負數,因為int是有符號的,當超過最大值時就變為負數

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

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迭代遍歷的能力。

4、採用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兩個變數值不想等導致的!

5、當集合擴容或者克隆時免不了對集合進行拷貝操作,那麼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方法。

6、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方法。

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

相關文章