java集合類型源碼解析之ArrayList

前言

做爲一個老碼農,不只要談架構、談併發,也不能忘記最基礎的語言和數據結構,所以特開闢這個系列的文章,爭取每月寫1~2篇關於java基礎知識的文章,以溫故而知新。java

如無特別之處,這個系列文章所使用的java版本都是1.8.0。數組

第一篇固然談ArrayList了,由於這是java最經常使用的list集合類型,它內部使用數組做爲存儲空間,在增長元素時可以自動增加。整體來講,ArrayList的實現比較簡單,這裏不羅列它的所有代碼,只看一些有意思的地方。安全

成員變量

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;
  • DEFAULT_CAPACITY 常量,初始話的時候若是沒有指定capacity,使用這個值;
  • EMPTY_ELEMENTDATA 空數組,全部空的ArrayList的elementData能夠共享此值,避免使用null;
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 也是elementData的值,表明初始化容量爲默認值(非0)的存儲空間,可是暫時尚未實際元素,等到須要增加存儲空間時,與EMPTY_ELEMENTDATA的策略不同;
  • elementData 提供存儲空間的數組;
  • size list長度,list的元素存儲在elementData[0~size)。

PS:說實話,DEFAULTCAPACITY_EMPTY_ELEMENTDATA的使用到底帶來了什麼好處,我沒想明白。數據結構

初始化

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
  • 第一種,指定初始容量;
  • 第二種,使用默認容量,此時DEFAULTCAPACITY_EMPTY_ELEMENTDATA派上用場;
  • 第三種,使用另外一個集合來初始化,使用了集合的toArray方法,值得注意的是,集合的toArray返回的不必定是Object[]類型。

空間增加

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

這是三個私有方法多線程

  • ensureCapacityInternal,在增加容量前加了一層檢查,若是elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,確保容量不小於默認容量;
  • ensureExplicitCapacity,檢查數組的容量是否確實須要擴充;
  • grow,執行數組擴容,先嚐試擴容一半,看是否知足需求;也就是說空間的擴容,不是嚴格按照輸入參數來的。

這裏有一個疑問,ensureExplicitCapacity方法的那句modCount++爲何會在if語句外執行.架構

增刪改查

基本操做方法有不少,這裏只列幾個有表明性的。併發

一、查找:dom

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

ArrayList查找元素的過程就是遍歷數組,並且null也是能夠查找的。函數

二、插入:ui

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

插入前作了範圍檢查,並確保容量,最後把插入位置以後的元素所有再日後挪一個位置;移動內存使用了System.arraycopy,這是一個native方法

值得注意的是,add方法並無直接調用modCount++,由於ensureCapacityInternal裏面調用了,因此我猜想modCount++放在ensureCapacityInternal裏面純碎就是爲了更方便,即便有些沒有修改數組的場景也會致使modCount被修改。

三、刪除:

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

把刪除位置以後的元素所有往前挪一個位置,注意的是,最後空出來的那個內存位置要置成null,不然會內存泄露

四、批量刪除

public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}
public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, true);
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

removeAll刪除集合參數裏的全部元素,反過來retainAll刪除不在集合參數裏面的全部元素,兩者都經過batchRemove來實現。

batchRemove使用兩個遊標w,r來執行連續的元素移動,w表明寫入位置,r表明讀取位置,讀取一個元素後判斷是否應該刪除,若是是那麼繼續讀下一個,不然寫入w位置。 這樣,到了最後w位置就是list的末尾,w~r之間的位置須要置成null。

值得注意的是,最後的收尾邏輯放到finally裏面,保證了必定程度的異常安全性。若是發生異常,那麼剩餘未掃描的元素(r位置以後的元素),要拷貝到w以後;這樣一來,batchRemove可能執行了一半而失敗,但ArrayList的狀態並無亂掉。

迭代器

ArrayList支持兩種迭代器,Iterator和ListIterator,後者是前者的加強版本,能夠向前移動、插入元素、返回元素索引。

這裏就解讀下Iterator實現,ListIterator是差很少的。

一、Iterator成員變量

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;

    public boolean hasNext() {
        return cursor != size;
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

Iterator實現爲ArrayList的非靜態內部內,所以能夠直接訪問ArrayList的成員字段,它只須要記住當前位置(cursor)便可。lastRet指向上一次next操做返回的元素位置,這個是很是有必要的,由於在迭代器上,若是要對元素作一些操做,都是針對這個位置的元素。

expectedModCount是ArrayListmodCount的快照,防止在迭代過程當中,意外對list執行修改。

二、next操做

@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];
}

很簡單,幾乎沒啥可解釋的。

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

remove操做會更新expectedModCount,這是一個容許的、意料之中的修改操做。

四、modCount
modCount定義在基類AbstractList當中,使用來追蹤list修改的,自身的值沒有實際意義。

modCount主要最用,就是在發生意外的修改時,快速失敗,而不是等到出現難以追蹤的數據狀態錯誤時才失敗。思路是,若是在執行一個操做的過程當中,若是不指望ArrayList被其餘操做所修改,那麼能夠在開始時記錄一下modCount快照,在執行操做的過程當中,經過比較ArrayList的modCount和這個快照來斷定ArrayList是否被修改了,而後拋出ConcurrentModificationException。

ConcurrentModificationException看起來像是多線程相關的,但實際上這裏和多線程一點關係都沒有,ArrayList不是線程安全的,modCount的設計也沒有針對多線程的意思。

SubList

ArrayList的subList能夠直接經過遊標鎖定原ArrayList的一個區段,從而避免了copy。
因爲SubList要實現List的所有接口,所有源碼比較多,這裏講解一下get方法的實現,其餘的都是相似的。

private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;
    private final int parentOffset;
    private final int offset;
    int size;

    SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
        this.parent = parent;
        this.parentOffset = fromIndex;
        this.offset = offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = ArrayList.this.modCount;
    }
    
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }
    public E set(int index, E e) {
        rangeCheck(index);
        checkForComodification();
        E oldValue = ArrayList.this.elementData(offset + index);
        ArrayList.this.elementData[offset + index] = e;
        return oldValue;
    }
}

一、首先,構造函數能夠看出來,SubList實際就是經過索引來操做原ArrayList的數據,只不過加一個偏移(offset)和長度限制(size)。

二、get方法檢查modCount,說明在SubList的生命週期內,指望parent ArrayList不會被修改。這也說明SubList只適合作臨時變量,不適合長期存活,除非原ArrayList是不變的。

AbstractList

AbstractList是ArrayList的基類,它也實現了Iterator,ListIterator,SubList的一個版本。不過AbstractList並不清楚存儲的實現細節,所以只能基於list的公共接口來實現,所以效率確定不佳。

好比AbstractList的Iterator的next方法是這樣實現的:

public E next() {
    checkForComodification();
    try {
        int i = cursor;
        E next = get(i);
        lastRet = i;
        cursor = i + 1;
        return next;
    } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

獲取元素用的是list.get方法:E next = get(i)

相關文章
相關標籤/搜索