java集合源碼分析(三):ArrayList

概述

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

在本篇文章,將在前文的基礎上,閱讀 List 最經常使用的實現類 Arraylist 的源碼,深刻了解這個「熟悉的陌生人」。java

1、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 是一個標記性的接口,實現了此接口的集合是容許被隨機訪問的。app

根據 JavaDoc 的說法,若是一個類實現了此接口,那麼:dom

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關鍵字使用小記

2、成員變量

在 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,是爲了防止在一些虛擬機中數組頭會被用於保持一些其餘信息。

3、構造方法

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

咱們通常使用比較多的是第一種,有時候會用第三種,實際上,若是咱們能夠估計到實際會添加多少元素,就可使用第二種構造器指定容量,避免擴容帶來的消耗。

4、擴容縮容

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
}

5、添加 / 獲取

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

6、刪除 / 修改

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 {
        // 遍歷本集合
        for (; r < size; r++)
            // 若是新增集合存在與本集合存在相同的元素,有兩種狀況
            // 1.removeAll,complement=false:直接跳過該元素
            // 2.retainAll,complement=true:把新元素插入原集合頭部
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // 若是上述操做中發生異常,則判斷是否已經完成本集合的遍歷
        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;
}

上述過程可能有點難一點理解,咱們假設這是 retailAll(),所以 complement=true,執行流程是這樣的:

batchRemove 的執行邏輯

同理,若是是removeAll(),那麼 w 就會始終爲0,最後就會把 elementData 的全部位置都設置爲 null。

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

7、迭代

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); // []

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

8、其餘

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

9、總結

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對節點進行刪除,由於刪除時數組「縮水」致使本來要刪除的下一下標對應的節點,卻落到了當前被刪除的節點對應的下標位置,致使被跳過。

    若是從隊尾反向刪除,就不會引發數組「縮水」,所以是正常的。

相關文章
相關標籤/搜索