源碼分析——ArrayList

前言

這篇文章咱們來分析一下另外一個容器類,ArrayList,它實現了List接口,是List的實現類,底層的實現是數組,下面就讓咱們來詳細分析一下。ps:前段時間由於學校實驗室課設外加期末考試等各類事情,博客很久沒更新了...java

1. 概覽

類間關係圖


ArrayList類實現了 RandomAccess 接口,所以支持隨機訪問。數組

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 複製代碼
數組的默認大小爲 10,而且能夠看到數據是存儲在elementData這個數組中的。
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;複製代碼

2. 擴容

添加元素時使用 ensureCapacityInternal() 方法來保證容量足夠,若是不夠時,須要使用 grow() 方法進行擴容,新容量的大小爲 oldCapacity + (oldCapacity >> 1),也就是 舊容量的 1.5 倍
擴容操做須要調用 Arrays.copyOf() 把原數組整個複製到新數組中,這是個遍歷複製的操做,操做的代價很高,所以最好在建立 ArrayList 對象時就指定大概的容量大小,減小擴容操做的次數。

須要注意的是,擴容操做也是一個線程不安全的操做。下面這行語句不是一個原子操做,因此在多線程環境下會出現數組越界訪問的問題。因此說ArrayList同HashMap同樣也是一個線程不安全的容器安全

elementData[size++] = e;複製代碼

下面是插入新元素和擴容操做的源碼:數據結構

//插入新元素
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //這裏會產生數組越界問題,由於下面這行語句並非一個原子操做
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(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 (newCapacity - minCapacity < 0)
        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);
}複製代碼

3. 刪除元素

須要調用 System.arraycopy() 將 index+1 後面的元素都複製到 index 位置上,該操做的 時間複雜度爲 O(N),能夠看出 ArrayList 刪除元素的代價是很是高的。

public E remove(int index) {
    rangeCheck(index);
    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;
}複製代碼

4. fail-fast

4.1 fail-fast簡介

fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操做時,就可能會產生fail-fast事件。
例如:當某一個線程A經過iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。多線程

4.2 ArrayList中的fail-fast

modCount 用來記錄 ArrayList 結構發生變化的次數。結構發生變化是指添加或者刪除至少一個元素的全部操做,或者是調整內部數組的大小,僅僅只是設置元素的值不算結構發生變化。dom

ArrayList在進行序列化或者迭代等操做時,須要比較操做先後 modCount 是否改變,若是改變了須要拋出 ConcurrentModificationException。

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

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}複製代碼

5. 序列化

ArrayList 基於數組實現,而且具備動態擴容特性,所以保存元素的數組不必定都會被使用,那麼就不必所有進行序列化。
保存元素的數組 elementData 使用 transient 修飾,該關鍵字聲明數組默認不會被序列化。

transient Object[] elementData; // non-private to simplify nested class access複製代碼
ArrayList 實現了 writeObject() 和 readObject() 來控制只序列化數組中有元素填充那部份內容。

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}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);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}複製代碼
序列化時須要使用 ObjectOutputStream 的 writeObject() 將對象轉換爲字節流並輸出。而 writeObject() 方法在傳入的對象存在 writeObject() 的時候會去反射調用該對象的 writeObject() 來實現序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,序列化和反序列化的原理相相似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);複製代碼

6. 總結

ArrayList從源碼上來看的確比HashMap要來的稍簡單一些,本文分析了ArrayList內部的數據結構與幾個經常使用的方法,如add()、remove()。同時討論了它爲何在多線程環境下是一個線程不安全的容器,若想要在多線程環境下使用,能夠考慮使用CopyOnWriteArrayList或者Vector這兩個容器,還簡單介紹了fail-fast在ArrayList中的應用以及ArrayList的序列化問題。this

最後也歡迎各位與我互相探討,指出本文的不足之處。spa

相關文章
相關標籤/搜索