Java1.8-ArrayList原始碼解析

weixin_34208283發表於2018-01-14

概述

  ArrayList可以理解為是一個可以動態擴容的陣列,因為本身就是使用陣列來實現的。

屬性

/**
 * 預設容量
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 初始空陣列
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 預設初始容量下的空陣列,這樣我們知道在新增第一個元素的時候,應該擴容多少
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * list實際儲存資料的陣列
 */
transient Object[] elementData; // non-private to simplify nested class access

/**
 * list的容量大小
 */
private int size;
方法

  ArrayList中的方法挺多,我們撿幾個比較重要的使用較多的來學習以下。

add方法

add方法實現大致流程:

  1. 判斷陣列容量是否為空,如果是,比較預設容量與實際容量大小,取最大值;
  2. 判斷實際所需容量如果大於陣列容量,擴容;
  3. 儲存資料,size加1;

原始碼:

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

    // 如果實際所需容量大於陣列容量,擴容
    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;
    // 如果擴容後的容量超過了系統預設的最大值:Integer.MAX_VALUE - 8,檢測是否溢位
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 使用Arrays的copyof方法將原陣列資料拷貝到新的陣列,並將新陣列賦值給變數elementData
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    // 如果溢位,提示異常,如果沒有溢位,實際所需容量是否超過系統預設的最大值,如果超過,返回Integer的最大值,如果沒有超過,返回系統預設的最大值
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
remove方法
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);
    // 將陣列最後一位置空,供GC呼叫
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

  其實remove方法本身沒什麼好說的,主要說以下removeAll。removeAll是刪除與另一個集合的交集。

removeAll方法的實現在於,先遍歷elementData,將elementData與另一個集合c沒有交集的資料,放置在elementData的下標的0到w段,然後再清除掉下標w到size-1之間的元素就行了(即設定為null)。

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;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
       // 如果c.contains提示異常
       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;
}
TrimToSize方法
  1. 我們對陣列擴容之後,有時候陣列的容量會大於實際所需要的容量,這時候如果我們想將陣列容量調整為實際所需要的容量,可以呼叫該方法。
  2. 比如記憶體緊張,或者我們可以確定不會再有元素新增進來時,也可以呼叫該方法來節省空間。
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}
ensureCapacity方法
  • 從add()與addAll()方法中可以看出,每當向陣列中新增元素時,都要去檢查新增元素後的個數是否會超出當前陣列的長度,如果超出,陣列將會進行擴容,以滿足新增資料的需求。
  • 在JDK8中,JDK提供了一個public的ensureCapacity方法讓我們可以手動設定ArrayList的容量,以減少上面這種遞增時呼叫再重新分配的數量;
public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}
其他

  由於ArrayList實現了RandomAccess, Cloneable, java.io.Serializable,所以支援隨機讀取,複製,序列化操作,對應一些方法:

/**
 * 按下標讀取
 */
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

/**
 * 複製
 */
public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

/**
 * 序列化
 */
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();
    }
}

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

問題

  1. ArrayList執行緒不安全體現在什麼地方?

  ArrayList在新增元素的時候,可能分為兩步:擴容,在該位置設定元素值。如果兩個執行緒同時到這一步,各自擴容之後,各自在該位置設定元素值,這樣同一個位置就被set了兩次,導致資料的汙染或者丟失;

  1. 如何是一個ArrayList執行緒安全?

可以藉助Collections,List list = Collections.synchronizedList(new ArrayList());

  1. JDK 5在java.util.concurrent裡引入了ConcurrentHashMap,在需要支援高併發的場景,我們可以使用它代替HashMap。但是為什麼沒有ArrayList的併發實現呢?難道在多執行緒場景下我們只有Vector這一種執行緒安全的陣列實現可以選擇麼?為什麼在java.util.concurrent 沒有一個類可以代替Vector呢?

  我認為在java.util.concurrent包中沒有加入併發的ArrayList實現的主要原因是:很難去開發一個通用並且沒有併發瓶頸的執行緒安全的List。
  像ConcurrentHashMap這樣的類的真正價值(The real point / value of classes)並不是它們保證了執行緒安全。而在於它們在保證執行緒安全的同時不存在併發瓶頸。舉個例子,ConcurrentHashMap採用了鎖分段技術和弱一致性的Map迭代器去規避併發瓶頸。
  所以問題在於,像“Array List”這樣的資料結構,你不知道如何去規避併發的瓶頸。拿contains() 這樣一個操作來說,當你進行搜尋的時候如何避免鎖住整個list?
  另一方面,Queue 和Deque (基於Linked List)有併發的實現是因為他們的介面相比List的介面有更多的限制,這些限制使得實現併發成為可能。
  CopyOnWriteArrayList是一個有趣的例子,它規避了只讀操作(如get/contains)併發的瓶頸,但是它為了做到這點,在修改操作中做了很多工作和修改可見性規則。 此外,修改操作還會鎖住整個List,因此這也是一個併發瓶頸。所以從理論上來說,CopyOnWriteArrayList並不算是一個通用的併發List。

上面第三個問題轉載自:
為什麼java.util.concurrent 包裡沒有併發的ArrayList實現?

總結

  ArrayList的優點是隨機讀取,缺點是插入資料時需要移動許多資料,而與之相對應的是LinkedList。我們將再下篇文章分析以一下。

相關文章