java集合原始碼分析(三):ArrayList

Createsequence發表於2020-12-02

概述

在前文:java集合原始碼分析(二):List與AbstractListjava集合原始碼分析(一):Collection 與 AbstractCollection 中,我們大致瞭解了從 Collection 介面到 List 介面,從 AbstractCollection 抽象類到 AbstractList 的層次關係和方法實現的大體過程。

在本篇文章,將在前文的基礎上,閱讀 List 最常用的實現類 Arraylist 的原始碼,深入瞭解這個“熟悉的陌生人”。

一、ArrayList 的類關係

image-20201201161347920

ArrayList 實現了三個介面,繼承了一個抽象類,其中 Serializable ,Cloneable 與 RandomAccess 介面都是用於標記的空介面,他的主要抽象方法來自於 List,一些實現來自於 AbstractList。

1.AbstractList 與 List

ArrayList 實現了 List 介面,是 List 介面的實現類之一,他通過繼承抽象類 AbstractList 獲得的大部分方法的實現。

比較特別的是,理論上父類 AbstractList 已經實現類 AbstractList 介面,那麼理論上 ArrayList 就已經可以通過父類獲取 List 中的抽象方法了,不必再去實現 List 介面。

網上關於這個問題的答案眾說紛紜,有說是為了通過共同的介面便於實現 JDK 代理,也有說是為了程式碼規範性與可讀性的,在 Stack Overflow 上 Why does LinkedHashSet extend HashSet and implement Set 一個據說問過原作者的老哥給出了一個 it was a mistake 的回答,但是這似乎不足以解釋為什麼幾乎所有的容器類都有類似的行為。事實到底是怎麼回事,也許只有真正的原作者知道了。

2.RandomAccess

RandomAccess 是一個標記性的介面,實現了此介面的集合是允許被隨機訪問的。

根據 JavaDoc 的說法,如果一個類實現了此介面,那麼:

for (int i=0, n=list.size(); i < n; i++)
    list.get(i);

要快於

for (Iterator i=list.iterator(); i.hasNext(); )
    i.next();

隨機訪問其實就是根據下標訪問,以 LinkedList 和 ArrayList 為例,LinkedList 底層實現是連結串列,隨機訪問需要遍歷連結串列,複雜度為 O(n),而 ArrayList 底層實現為陣列,隨機訪問直接通過下標去定址就行了,複雜度是O(1)。

當我們需要指定迭代的演算法的時候,可以通過實現類是否實現了 RandomAccess 介面來選擇對應的迭代方式。在一些方法操作集合的方法裡(比如 AbstractList 中的 subList),也根據這點做了一些處理。

3.Cloneable

Cloneable 介面表示它的實現類是可以被拷貝的,根據 JavaDoc 的說法:

一個類實現Cloneable介面,以表明該通過Object.clone()方法為該類的例項進行逐域複製是合法的。

在未實現Cloneable介面的例項上呼叫Object的clone方法會導致丟擲CloneNotSupportedException異常。

按照約定,實現此介面的類應使用公共方法重寫Object.clone()。

簡單的說,如果一個類想要使用Object.clone()方法以實現物件的拷貝,那麼這個類需要實現 Cloneable 介面並且重寫 Object.clone()方法。值得一提的是,Object.clone()預設提供的拷貝是淺拷貝,淺拷貝實際上沒有拷貝並且建立一個新的例項,通過淺拷貝獲得的物件變數其實還是指標,指向的還是原來那個記憶體地址。深拷貝的方法需要我們自己提供。

4.Serializable

Serializable 介面也是一個標記性介面,他表明實現類是可以被序列化與反序列化的。

這裡提一下序列化的概念。

序列化是指把一個 Java 物件變成二進位制內容的過程,本質上就是把物件轉為一個 byte[] 陣列,反序列化同理。

當一個 java 物件序列化以後,就可以得到的 byte[] 儲存到檔案中,或者把 byte[] 通過網路傳輸到遠端,這樣就相當於把 Java 物件儲存到檔案或者通過網路傳輸出去了。

值得一提的是,針對一些不希望被儲存到檔案,或者以位元組流的形式被傳輸的私密資訊,java 提供了 transient 關鍵字,被其標記的屬性不會被序列化。比如在 AbstractList 裡,之前提到的併發修改檢查中用於記錄結構性操作次數的變數 modCount,還有下面要介紹到的 ArrayList 的底層陣列 elementData 就是被 transient 關鍵字修飾的。

更多的內容可以參考大佬的博文:Java transient關鍵字使用小記

二、成員變數

在 ArrayList 中,一共有七個成員變數:

private static final long serialVersionUID = 8683452581122892189L;

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

/**
 * 用於空例項的共享空陣列例項
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 共享的空陣列例項,用於預設大小的空例項。我們將此與EMPTY_ELEMENTDATA區別開來,以瞭解新增第一個元素時要擴容陣列到多大。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 儲存ArrayList的元素的陣列緩衝區。 ArrayList的容量是此陣列緩衝區的長度。新增第一個元素時,任何符合
 * elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空ArrayList都將擴充套件為DEFAULT_CAPACITY。
 */
transient Object[] elementData;

/**
 * ArrayList的大小(它包含的元素數)
 */
private int size;

/**
 * 要分配的最大陣列大小
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

我們來一個一個的解釋他們的作用。

1.serialVersionUID

private static final long serialVersionUID = 8683452581122892189L;

用於序列化檢測的 UUID,我們可以簡單的理解他的作用:

當序列化以後,serialVersionUID 會被一起寫入檔案,當反序列化的時候,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException。

更多內容仍然可以參考大佬的博文:java類中serialversionuid 作用 是什麼?舉個例子說明

2.DEFAULT_CAPACITY

預設容量,如果例項化的時候沒有在構造方法裡指定初始容量大小,第一個擴容就會根據這個值擴容。

3.EMPTY_ELEMENTDATA

一個空陣列,當呼叫構造方法的時候指定容量為0,或者其他什麼操作會導致集合內陣列長度變為0的時候,就會直接把空陣列賦給集合實際用於存放資料的陣列 elementData

4.DEFAULTCAPACITY_EMPTY_ELEMENTDATA

也是一個空陣列,不同於 EMPTY_ELEMENTDATA 是指定了容量為0的時候會被賦給elementData,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA是在不指定容量的時候才會被賦給 elementData,而且新增第一個元素的時候就會被擴容。

DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA都不影響實際後續往裡頭新增元素,兩者主要表示一個邏輯上的區別:前者表示集合目前為空,但是以後可能會新增元素,而後者表示這個集合一開始就沒打算存任何東西,是個容量為0的空集合。

5.elementData

實際存放資料的陣列,當擴容或者其他什麼操作的時候,會先把資料拷貝到新陣列,然後讓這個變數指向新陣列。

6.size

集合中的元素數量(注意不是陣列長度)。

7.MAX_ARRAY_SIZE

允許的最大陣列長度,之所以等於 Integer.MAX_VALUE - 8,是為了防止在一些虛擬機器中陣列頭會被用於保持一些其他資訊。

三、構造方法

ArrayList 中提供了三個構造方法:

  • ArrayList()
  • ArrayList(int initialCapacity)
  • ArrayList(Collection<? extends E> c)
// 1.構造一個空集合
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 2.構造一個具有指定初始容量的空集合
public ArrayList(int initialCapacity) {
    // 判斷指定的初始容量是否大於0
    if (initialCapacity > 0) {
        // 若大於0,則直接指定elementData陣列的長度
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 若等於0,將EMPTY_ELEMENTDATA賦給elementData
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 小於0,拋異常
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

// 3.構造一個包含指定集合所有元素的集合
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    // 判斷傳入的集合是否為空集合
    if ((size = elementData.length) != 0) {
        // 確認轉為的集合底層實現是否也是Objcet陣列
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果是空集合,將EMPTY_ELEMENTDATA賦給elementData
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

我們一般使用比較多的是第一種,有時候會用第三種,實際上,如果我們可以估計到實際會新增多少元素,就可以使用第二種構造器指定容量,避免擴容帶來的消耗。

四、擴容縮容

ArrayList 的可擴充套件性是它最重要的特性之一,在開始瞭解其他方法前,我們需要先了解一下 ArrayList 是如何實現擴容和縮容的。

0.System.arraycopy()

在這之前,我們需要理解一下擴容縮容所依賴的核心方法 System.arraycopy()方法:

/**
 * 從一個源陣列複製元素到另一個陣列,如果該陣列指定位置已經有元素,就使用複製過來的元素替換它
 *
 * @param src 要複製的源陣列
 * @param srcPos 要從源陣列哪個下標開始複製
 * @param dest 要被移入元素的陣列
 * @param destPos  要從被移入元素陣列哪個下標開始替換
 * @param length 複製元素的個數
 */   
arraycopy(Object src,  int  srcPos,
          Object dest, int destPos,
          int length)

我們舉個例子,假如我們現在有 arr1 = {1,2,3,4,5}arr2 = {6,7,8,9,10},現在我們使用 arraycopy(arr1, 0, arr2, 0, 2),則意為:

使用從 arr1 索引為 0 的元素開始,複製 2 個元素,然後把這兩個元素從 arr2 陣列中索引為 0 的地方開始替換原本的元素,

int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = {6, 7, 8, 9, 10};
System.arraycopy(arr1, 0, arr2, 0, 2);
// arr2 = {1,2,8,9,10}

1.擴容

雖然在 AbstractCollection 抽象類中已經有了簡單的擴容方法 finishToArray(),但是 ArrayList 沒有繼續使用它,而是自己重新實現了擴容的過程。ArrayList 的擴容過程一般發生在新增元素上。

會引起ArrayList擴容的方法

我們以 add() 方法為例:

public boolean add(E e) {
    // 判斷新元素加入後,集合是否需要擴容
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

(1)檢查是否初次擴容

我們知道,在使用建構函式構建集合的時候,如果未指定初始容量,則內部陣列 elementData 會被賦上預設空陣列 DEFAULTCAPACITY_EMPTY_ELEMENTDATA

因此,當我們呼叫 add()時,會先呼叫 ensureCapacityInternal()方法判斷elementData 是否還是DEFAULTCAPACITY_EMPTY_ELEMENTDATA如果是,說明建立的時候沒有指定初始容量,而且沒有被擴容過,因此要保證集合被擴容到10或者更大的容量:

private void ensureCapacityInternal(int minCapacity) {
    // 判斷是否還是初始狀態
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 擴容到預設容量(10)或更大
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
	
    ensureExplicitCapacity(minCapacity);
}

(2)檢查是否需要擴容

當決定好了第一次擴容的大小,或者elementData被擴容過最少一次以後,就會進入到擴容的準備過程ensureExplicitCapacity(),在這個方法中,將會增加操作計數器modCount,並且保證新容量要比當前陣列長度大

private void ensureExplicitCapacity(int minCapacity) {
    // 擴容也是結構性操作,modCount+1
    modCount++;

    // 判斷最小所需容量是否大於當前底層陣列長度
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

(3)擴容

最後進入真正的擴容方法 grow()

// 擴容
private void grow(int minCapacity) {
    // 舊容量為陣列當前長度
    int oldCapacity = elementData.length;
    // 新容量為舊容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新容量小於最小所需容量(size + 1),就以最小所需容量作為新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量大於允許的最大容量,就再判斷能否再繼續擴容
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 擴容完畢,將舊陣列的資料拷貝到新陣列上
    elementData = Arrays.copyOf(elementData, newCapacity);
}

這裡可能有人會有疑問,為什麼oldCapacity要等於elementData.length而不可以是 size()呢?

因為在 ArrayList,既有需要徹底移除元素並新建陣列的真刪除,也有隻是對應下標元素設定為 null 的假刪除,size()實際計算的是有元素個數,因此這裡需要使用elementData.length來了解陣列的真實長度。

回到擴容,由於 MAX_ARRAY_SIZE已經是理論上允許的最大擴容大小了,如果新容量比MAX_ARRAY_SIZE還大,那麼就涉及到一個臨界擴容大小的問題,hugeCapacity()方法被用於決定最終允許的容量大小

private static int hugeCapacity(int minCapacity) {
    // 是否發生溢位
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError
        ("Required array size too large");
    // 判斷最終大小是MAX_ARRAY_SIZE還是Integer.MAX_VALUE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

ArrayList 的 hugeCapacity()AbstractCollection抽象類中的 hugeCapacity()是完全一樣的,當 minCapacity > MAX_ARRAY_SIZE的情況成立的時候,說明現在的當前元素個數size容量已經等於 MAX_ARRAY_SIZE,陣列已經極大了,這個時候再進行拷貝操作會非常消耗效能,因此最後一次擴容會直接擴到 Integer.MAX_VALUE,如果再大就只能溢位了。

以下是擴容的流程圖:

ArrayList 的擴容流程

2.縮容

除了擴容,ArrayList 還提供了縮容的方法 trimToSize(),但是這個方法不被任何其他內部方法呼叫,只能由程式猿自己去呼叫,主動讓 ArrayList 瘦身,因此在日常使用中並不是很常見。

public void trimToSize() {
    // 結構性操作,modCount+1
    modCount++;
    // 判斷當前元素個數是否小於當前底層陣列的長度
    if (size < elementData.length) {
        // 如果長度為0,就變為EMPTY_ELEMENTDATA空陣列
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            // 否則就把容量縮小為當前的元素個數
            : Arrays.copyOf(elementData, size);
    }
}

3.測試

我們可以藉助反射,來看看 ArrayList 的擴容和縮容過程:

先寫一個通過反射獲取 elementData 的方法:

// 通過反射獲取值
public static void getEleSize(List<?> list) {
    try {

        Field ele = list.getClass().getDeclaredField("elementData");
        ele.setAccessible(true);
        Object[] arr = (Object[]) ele.get(list);
        System.out.println("當前elementData陣列的長度:" + arr.length);

    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

然後實驗看看:

public static void main(String[] args) {
    // 第一次擴容
    ArrayList<String> list = new ArrayList<>();
    getEleSize(list); // 當前elementData陣列的長度:0
    list.add("aaa");
    getEleSize(list); // 當前elementData陣列的長度:10

    // 指定初始容量為0的集合,進行第一次擴容
    ArrayList<String> emptyList = new ArrayList<>(0);
    getEleSize(emptyList); // 當前elementData陣列的長度:0
    emptyList.add("aaa");
    getEleSize(emptyList); // 當前elementData陣列的長度:1

    // 擴容1.5倍
    for (int i = 0; i < 10; i++) {
        list.add("aaa");
    }
    getEleSize(list); // 當前elementData陣列的長度:15

    // 縮容
    list.trimToSize();
    getEleSize(list);// 當前elementData陣列的長度:11
}

五、新增 / 獲取

1.add

public boolean add(E e) {
    // 如果需要就先擴容
    ensureCapacityInternal(size + 1);
    // 新增到當前位置的下一位
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    // 若 index > size || index < 0 則拋 IndexOutOfBoundsException 異常
    rangeCheckForAdd(index);
    // 如果需要就先擴容
    ensureCapacityInternal(size + 1);
    // 把原本 index 下標以後的元素集體後移一位,為新插入的陣列騰位置
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

新增的原理比較簡單,實際上就是如果不指定下標就插到陣列尾部,否則就先建立一個新陣列,然後把舊陣列的資料移動到新陣列,並且在這個過程中提前在新陣列上留好要插入的元素的空位,最後再把元素插入陣列。後面的增刪操作基本都是這個原理。

ArrayList 的新增

2.addAll

public boolean addAll(Collection<? extends E> c) {
    // 將新集合的陣列取出
    Object[] a = c.toArray();
    int numNew = a.length;
    // 如有必要就擴容
    ensureCapacityInternal(size + numNew);
    // 將新陣列拼接到原陣列的尾部
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    // 先擴容
    ensureCapacityInternal(size + numNew);

    // 判斷是否需要移動原陣列
    int numMoved = size - index;
    if (numMoved > 0)
        // 則將原本 index 下標以後的元素移到 index + numNew 的位置
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

3.get

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

// 根據下標從陣列中取值,被使用在get(),set(),remove()等方法中
E elementData(int index) {
    return (E) elementData[index];
}

六、刪除 / 修改

1.remove

public E remove(int index) {
    // 若 index >= size 會丟擲 IndexOutOfBoundsException 異常
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);
	
    // 判斷是否需要移動陣列
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 把元素尾部位置設定為null,便於下一次插入
    elementData[--size] = null;

    return oldValue;
}

public boolean remove(Object o) {
    // 如果要刪除的元素是null
    if (o == null) {
        for (int index = 0; index < size; index++)
            // 移除第一位為null的元素
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 如果要刪除的元素不為null
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

這裡有用到一個fastRemove()方法:

// fast 的地方在於:跳過邊界檢查,並且不返回刪除的值
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null;
}

比較有趣的地方在於,remove()的時候檢查的是index >= size,而 add()的時候檢查的是 index > size || index < 0,可見新增的時候還要看看 index 是否小於0。

原因在於 add()在校驗完以後,立刻就會呼叫System.arraycopy(),由於這是個 native 方法,所以出錯不會拋異常;而 remve() 呼叫完後,會先使用 elementData(index)取值,這時如果 index<0 會直接拋異常。

2.clear

比較需要注意的是,相比起remove()方法,clear()只是把陣列的每一位都設定為null,elementData的長度是沒有改變的:

public void clear() {
    modCount++;
	// 把陣列每一位都設定為null
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

3.removeAll / retainAll

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

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

這兩個方法都依賴於 batchRemove()方法:

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        // 1.遍歷本集合
        for (; r < size; r++)
            // 如果新增集合存在與本集合存在相同的元素,有兩種情況
            // 1.removeAll,complement=false:直接跳過該元素
            // 2.retainAll,complement=true:把新元素插入原集合頭部
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // 2.如果上述操作中發生異常,則判斷是否已經完成本集合的遍歷
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // 3.將陣列剩下的位置都改為null
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

上述這三個過程可能有點難一點理解,我們假設這是 retailAll(),因此 complement=true,執行流程是這樣的:

batchRemove 的執行邏輯

同理,如果是removeAll(),那麼 w 就會始終為0,最後就會把 elementData 的所有位置都設定為 null。

也就是說,在遍歷過程中如果不發生異常,就會跳過第二步,直接進入第三步。

當然,這是沒有發生異常的情況,因此遍歷完成後 r = size,那麼如果遍歷到 r = 2,也就是進入 if 分支後,程式發生了異常,尚未完成遍歷就進入了 finallly 塊,就會先進入第二步,也就是下面的流程:

batchRemove 發生異常時的執行邏輯

最終陣列會變為 {C,C,D,null} ,只有最後一個 D 被刪除。

4.removeIf

這個是 JDK8 以後的新增方法:

public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    
    int removeCount = 0;
    final BitSet removeSet = new BitSet(size);
    final int expectedModCount = modCount;
    final int size = this.size;
    // 遍歷集合,同時做併發修改檢查
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        // 使用 lambda 表示式傳入的匿名方法校驗元素
        if (filter.test(element)) {
            removeSet.set(i);
            removeCount++;
        }
    }
    // 併發修改檢測
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }

    // 是否有有需要刪除的元素
    final boolean anyToRemove = removeCount > 0;
    if (anyToRemove) {
        // 新容量為舊容量-刪除元素數量
        final int newSize = size - removeCount;
        // 把被刪除的元素留下的空位“補齊”
        for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
            i = removeSet.nextClearBit(i);
            elementData[j] = elementData[i];
        }
        // 將刪除的位置設定為null
        for (int k=newSize; k < size; k++) {
            elementData[k] = null;
        }
        this.size = newSize;
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

    return anyToRemove;
}

5.set

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

6.replaceAll

這也是一個 JDK8 新增的方法:

public void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final int expectedModCount = modCount;
    final int size = this.size;
    // 遍歷,並使用lambda表示式傳入的匿名函式處理每一個元素
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        elementData[i] = operator.apply((E) elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

七、迭代

1.iterator / listIterator

ArrayList 重新實現了自己的迭代器,而不是繼續使用 AbstractList 提供的迭代器。

和 AbstracList 一樣,ArrayList 實現的迭代器內部類仍然是基礎迭代器 Itr 和加強的迭代器 ListItr,他和 AbstractList 中的兩個同名內部類基本一樣,但是針對 ArrayList 的特性對方法做了一些調整:比如一些地方取消了對內部方法的呼叫,直接對 elementData 下標進行操作等。

這一塊可以參考上篇文章,或者看看原始碼,這裡就不贅述了。

2.forEach

這是一個針對 Collection 的父介面 Iterable 介面中 forEach 方法的重寫。在 ArrayList 的實現是這樣的:

public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    // 獲取 modCount
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        // 遍歷元素並呼叫lambda表示式處理元素
        action.accept(elementData[i]);
    }
    // 遍歷結束後才進行併發修改檢測
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

3.迭代刪除存在的問題

到目前為止,我們知道有三種迭代方式:

  • 使用 iterator()listIterator()獲取迭代器;
  • forEach()
  • for 迴圈。

如果我們在迴圈中刪除集合的節點,只有迭代器的方式可以正常刪除,其他都會出問題。

forEach

我們先試試使用 forEach()

ArrayList<String> arrayList1 = new ArrayList<>(Arrays.asList("A","B","C","D"));
arrayList1.forEach(arrayList1::remove); // java.util.ConcurrentModificationException

可見會丟擲 ConcurrentModificationException異常,我們回到 forEach()的程式碼中:

public void forEach(Consumer<? super E> action) {
    // 獲取 modCount
    final int expectedModCount = modCount;
    
    ... ...
    for () {
        // 遍歷元素並呼叫lambda表示式處理元素
        action.accept(elementData[i]);
    }
    ... ...
        
    // 遍歷結束後才進行併發修改檢測
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

由於在方法執行的開始就令 expectedModCount= modCount,等到迴圈處理結束後才進行 modCount != expectedModCount的判斷,這樣如果我們在匿名函式中對元素做了一些結構性操作,導致 modCount增加,最後就會在檢測就會發現迴圈結束以後的 modCount 與一開始得到的 modCount不一致,所以會丟擲 ConcurrentModificationException異常。

for迴圈

先寫一個例子:

ArrayList<String> arrayList1 = new ArrayList<>(Arrays.asList("A","B","C","D"));
for (int i = 0; i < arrayList1.size(); i++) {
    arrayList1.remove(i);
}
System.out.println(arrayList1); // [B, D]

可以看到,B 和 C 的刪除被跳過了。實際上,這個問題和 AbstractList 的迭代器 Itr 中 remove() 方法遇到的問題有點像:

在 AbstractList 的 Itr 中,每次刪除都會導致陣列的“縮短”,在被刪除元素的前一個元素會在 remove()後“補空”,落到被刪除元素下標所對應的位置上,也就是說,假如有 a,b 兩個元素,刪除了下標為0的元素a以後,b就會落到下標為0的位置

上文提到 ArrayList 的 remove() 呼叫了 fastRemove()方法,我們可以看看他是否就是罪魁禍首:

private void fastRemove(int index) {
    ... ...
    // 如果不是在陣列末尾刪除
    if (numMoved > 0)
        // 陣列被縮短了
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null;
}

所以陣列“縮短”導致的元素下標變動就是問題的根源,換句話說,如果不呼叫 System.arraycopy()方法,理論上就不會引起這個問題,所以我們可以試試反向刪除:

ArrayList<String> arrayList1 = new ArrayList<>(Arrays.asList("A","B","C","D"));
// 反向刪除
for (int i = arrayList1.size() - 1; i >= 0; i--) {
    arrayList1.remove(i);
}
System.out.println(arrayList1); // []

可見反向刪除是沒有問題的。

八、其他

1.indexOf / lastIndexOf / contains

相比起 AbstractList ,ArrayList 不再使用迭代器,而是改寫成了根據下標進行for迴圈:

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

// lastIndexOf
public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

至於 contains() 方法,由於已經實現了 indexOf(),自然不必繼續使用 AbstractCollection 提供的迭代查詢了,而是改成了:

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

2.subList

subList()iterator()一樣,也是返回一個特殊的內部類 SubList,在 AbstractList 中也已經有相同的實現,只不過在 ArrayList 裡面進行了一些改進,大體邏輯和 AbstractList 中是相似的,這部分內容在前文已經有提到過,這裡就不再多費筆墨。

3.sort

public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

java 中集合排序要麼元素類實現 Comparable 介面,要麼自己寫一個 Comparator 比較器。這個函式的引數指明瞭型別是比較器,因此只能傳遞自定義的比較器,在 JDK8 以後,Comparator 類提供的了一些預設實現,我們可以以類似 Comparator.reverseOrder() 的方式去呼叫,或者直接用 lambda 表示式傳入一個匿名方法。

4.toArray

toArray() 方法在 AbstractList 的父類 AbstractCollection 中已經有過基本的實現,ArrayList 根據自己的情況重寫了該方法:

public Object[] toArray() {
    // 直接返回 elementData 的拷貝
    return Arrays.copyOf(elementData, size);
}

public <T> T[] toArray(T[] a) {
    // 如果傳入的素組比本集合的元素數量少
    if (a.length < size)
        // 直接返回elementData的拷貝
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    // 把elementData的0到size的元素覆蓋到傳入陣列
    System.arraycopy(elementData, 0, a, 0, size);
    // 如果傳入陣列元素比本集合的元素多
    if (a.length > size)
        // 讓傳入陣列size位置變為null
        a[size] = null;
    return a;
}

5.clone

ArrayList 實現了 Cloneable 介面,因此他理當有自己的 clone()方法:

public Object clone() {
    try {
        // Object.clone()拷貝ArrayList
        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);
    }
}

要注意的是,通過 clone()得到的 ArrayList 不是同一個例項,但是使用 Arrays.copyOf()得到的元素物件是同一個物件。我們舉個例子:

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    ArrayList<MyBean> arrayList1 = new ArrayList<>(Arrays.asList(new MyBean()));
    ArrayList<MyBean> arrayList2 = (ArrayList<MyBean>) arrayList1.clone();
    System.out.println(arrayList1); // [$MyBean@782830e]
    System.out.println(arrayList2); // [$MyBean@782830e]
    System.out.println(arrayList1 == arrayList2); // false

    arrayList1.add(new MyBean());
    System.out.println(arrayList1); // [MyBean@782830e, $MyBean@470e2030]
    arrayList2.add(new MyBean());
    System.out.println(arrayList2); // [$MyBean@782830e, $MyBean@3fb4f649]
}

public static class MyBean {}

可以看到,arrayList1 == arrayList2是 false,說明是 ArrayList 兩個例項,但是內部的第一個 MyBean 都是 $MyBean@782830e,說明是同一個例項。

6.isEmpty

public boolean isEmpty() {
    return size == 0;
}

九、總結

ArrayList 底層是 Object[] 陣列,被 RandomAccess 介面標記,具有根據下標高速隨機訪問的功能;

ArrayList 擴容是擴大1.5倍,只有構造方法指定初始容量為0時,才會在第一次擴容出現小於10的容量,否則第一次擴容後的容量必然大於等於10;

ArrayList 有縮容方法trimToSize(),但是自身不會主動呼叫。當呼叫後,容量會縮回實際元素數量,最小會縮容至預設容量10;

ArrayList 的新增可能會因為擴容導致陣列“膨脹”,同理,不是所有的刪除都會引起陣列“縮水”:當刪除的元素是隊尾元素,或者clear()方法都只會把下標對應的地方設定為null,而不會真正的刪除陣列這個位置;

ArrayList 在迴圈中刪除——準確的講,是任何會引起 modCount變化的結構性操作——可能會引起意外:

  • forEach()刪除元素會拋ConcurrentModificationException異常,因為 forEach()在迴圈開始前就獲取了 modCount,但是到迴圈結束才比較舊 modCount和最新的 modeCount

  • 在 for 迴圈裡刪除實際上是以步長為2對節點進行刪除,因為刪除時陣列“縮水”導致原本要刪除的下一下標對應的節點,卻落到了當前被刪除的節點對應的下標位置,導致被跳過。

    如果從隊尾反向刪除,就不會引起陣列“縮水”,因此是正常的。

相關文章