ArrayList原始碼分析(JDK1.8)

軒轅慕雨發表於2021-02-03

概述

ArrayList底層是基於陣列實現的,並且支援動態擴容的動態陣列(變長的集合類)。ArrayList允許空值和重複的元素,當向ArrayList中新增元素數量大於其底層陣列容量時,會通過擴容機制重新生成一個容量更大的陣列。另外,由於ArrayList底層資料結構是陣列,所以保證了在O(1)複雜度下完成隨機查詢操作。ArrayList是非執行緒安全的,在併發環境下,多個執行緒同時操作ArrayList會引發不可預知的錯誤。

從上面的類圖可以看出,ArrayList實現了4個介面和繼承了1個抽象類,分別是:

  • List介面:主要提供了陣列的新增、刪除、修改、迭代遍歷等操作;
  • Cloneable介面:標識克隆操作;
  • Serializable介面:標識可序列列化操作;
  • RandomAccess介面:標識可隨機訪問操作;
  • 繼承AbstractList抽象類,主要提供迭代遍歷相關操作。

屬性

ArrayList的屬性比較少,只有兩個屬性:elementData和size。

1 private static final int DEFAULT_CAPACITY = 10;
2 
3 private static final Object[] EMPTY_ELEMENTDATA = {};
4 
5 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
6 // 底層存放元素的陣列
7 transient Object[] elementData;
8 // elementData陣列中元素的數量 
9 private int size;

構造方法

ArrayList有3個構造方法,分別如下:

  • ArrayList(int initialCapcity):根據傳入的initialCapacity初始化容量來建立elementData陣列;
  • ArrayList():無參構造方法;
  • ArrayList(Collection<? extends E> c):使用傳入的集合C作為ArrayList的elementData;
// 1. ArrayList(int initialCapacity)構造方法
// 需要注意:當初始化容量initialCapacity為0時,使用EMPTY_ELEMENTDATA物件建立一個空陣列,在新增元素的時候,會進行擴容建立需要的陣列 public ArrayList(int initialCapacity) { if (initialCapacity > 0) {
       // 初始化容量大於0,建立Object陣列
this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) {
       // 初始化容量為0時,使用EMPTY_ELEMENTDATA物件
this.elementData = EMPTY_ELEMENTDATA; } else {
       // 引數異常
throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity); } } // 2. ArrayList()無參構造方法 public ArrayList() {
     // 實際建立的時候是空陣列,在首次新增元素的時候,才會初始化容量為10的陣列
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 3. ArrayList(Collection<? extends E> c)構造方法 public ArrayList(Collection<? extends E> c) {
     // elementData指向c.toArray() elementData
= c.toArray(); if ((size = elementData.length) != 0) {
       // 如果集合元素的型別不是Object.class型別,則拷貝成Object.class型別
// c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }

插入元素

對於陣列(線性表)結構,插入操作分為兩種情況:① 在元素序列尾部插入;② 指定位置插入;對於ArrayList插入方法有4個:

  • public boolean add(E e):在陣列尾部插入元素;
  • public void add(int index, E element):在陣列指定index位置插入元素;
  • public boolean addAll(Collection<? extends E> c):在陣列尾部插入多個元素;
  • public boolean addAll(int index, Collection<? extends E> c):在陣列指定index位置插入多個元素;
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}

 主要關注ensureCapacityInternal方法具體做了什麼事情:現有的陣列容量是否支援新增元素的需求,如果不滿足,則考慮擴容操作

通過底層原始碼,我們看看JDK的設計者是如何考慮的:

STEP-1:首先是ensureCapacityInternal(int minCapacity)方法,該方法的主要功能是 確保陣列內部容量足以滿足本次插入操作

該方法實際是首先呼叫calculateCapacity(Object[] elementData, int minCapacity)方法,計算滿足本次插入操作所需要的容量,然後呼叫ensureExplicitCapacity(int minCapacity)方法,保證容量足夠

/**
* 確保陣列容量足夠新增元素
*
* @param minCapacity 最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
    // 計算容量
    int capacity = calculateCapacity(elementData, minCapacity);
    // 確保容量足夠 -- 如不足夠則觸發擴容
    ensureExplicitCapacity(capacity);
}

STEP-2:考量calculateCapacity(Object[] elementData, int minCapacity)方法是如何計算本次插入操作所需容量的。

如果elementData陣列是DEFAULTCAPACITY_EMPTY_ELEMENTDATA:沒有指定初始化容量時;則會判斷是最小容量minCapacity和DEFAULT_CAPACITY的大小,取兩者中較大者。

/**
* 計算容量
* @param elementData 元素陣列
* @param minCapacity 最小容量
* @return int 計算後的陣列容量大小
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是DEFAULT_CAPACITY ,那麼就會從10開
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // DEFAULT_CAPACITY 定義:private static final int DEFAULT_CAPACITY = 10;
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 如果不是則直接返回需要的最小容量
    return minCapacity;
}

STEP-3:考量ensureExplicitCapacity 如何確保陣列容量足夠本次新增操作的:當所需的最小容量minCapacity大於elementData.length陣列長度時,則觸發grow()執行擴容。

/**
  * 確保陣列剩餘容量足夠
  * @param minCapacity 最小容量
  */
private void ensureExplicitCapacity(int minCapacity) {
    // 在父類 AbstractList中定義了 用以記錄陣列修改的次數(注意是次數)
    modCount++;

    // minCapacity代表的是本次(新增)操作所需要的最小容量, elementData.length代表的是當前元素陣列的長度
    // 可能寫成 minCapacity > elementData.length 更好理解
    if (minCapacity - elementData.length > 0) {
        // 擴容可指定最小的容量(滿足當前需求)
        grow(minCapacity);
    }
}
/**
  * 擴容機制
  * 舊容量經過運算擴充套件為1.5倍後與最小容量minCapacity進行比較
  * 如果大於則採用舊容量擴充套件1.5倍後的大小,否則採用最小容量minCapacity
  * @param minCapacity 新增操作所需最小容量
  */
private void grow(int minCapacity) {
    // 舊容量大小 elementData陣列的長度
    int oldCapacity = elementData.length;
    // 新容量大小 == 舊 + 舊的位運算 (向右移動1位)   大致上是原大小的1.5倍
// ArrayList擴容機制
int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果計算出來的新容量小於 指定的最小容量,則建立指定的最小容量大小的新陣列 // 可理解為 newCapacity < minCapacity 不知道為啥會喜歡寫成減法的形式。。。 if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } // 如果新容量大於最大值(會出現記憶體不足的情況) // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 if (newCapacity - MAX_ARRAY_SIZE > 0) { // hugeCapicaty方法用以當 newCapacity超過指定範圍的時候,確保建立的陣列不會溢位 newCapacity = hugeCapacity(minCapacity); } // 陣列拷貝,將原有的資料搬到新的陣列上 elementData = Arrays.copyOf(elementData, newCapacity); }

綜述:經過上述過程的研討,我們知道ArrayList插入操作的整個過程:首先計算插入元素所需的最小容量;然後判斷當前elementData陣列是否支援本次插入操作,當容量不足時則觸發擴容機制,將原有陣列的元素拷貝到新陣列上,然後往後追加新的元素。【備註,其他3個插入操作過程基本類似,可自行分析原始碼】

查詢元素

public int indexOf(Object o):查詢首個為指定元素o的位置,原始碼如下:

/**
  * 查詢首個為指定元素 o 的位置
  * @param o 被查詢元素
  * @return int 如果存在則返回對應index 不存在返回-1
  */
@Override
public int indexOf(Object o) {
    // 從這裡我們其實也可以看出,ArrayList是支援儲存null值的
    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;
            }
        }
    }
    // 不存在則返回 -1
    return -1;
}

刪除元素

刪除元素的方法主要有4個,分別如下:

  • public E remove(int index):移除指定位置的元素,並返回該位置的原元素;
  • public boolean remove(Object o):移除首個為o的元素,並返回是否移除成功;
  • protected void removeRange(int fromIndex, int toIndex):批量移除[fromIndex, toIndex)內的多個元素,注意包左不含右
  • public boolean removeAll(Collection<?> c):批量移除指定的多個元素;

public E remove(int index) 繼承於 AbstractList 抽象類,刪除指定位置的元素,並返回該位置上的原元素。

/**
  * 刪除指定位置index的元素,並返回該元素
  * @param index
  * @return E
  */
public E remove(int index) {
    // index 合法性校驗,不合法則丟擲相關異常
    rangeCheck(index);
    // 修改陣列的次數 + 1
    modCount++;
    // 獲取index下標對應的value,elementData方法其實就是 return (E) elementData[index]
    E oldValue = elementData(index);

    // 每刪除一個元素,都需要對原有的陣列進行移動,所以從這裡也可以看出ArrayList並不是特別適合於刪除操作比較多的場景~
    // 需要移動的元素的數量
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    // 陣列的最後一個位置設定為null  幫助GC
    elementData[--size] = null;

    return oldValue;
}

private void rangeCheck(int index) {
    // index 合法性校驗
    if (index >= size) {
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
}

【注意】:刪除原始碼是比較簡單易讀的,注意一點:凡是涉及到index的操作首先需要引數合法性檢查,然後再獲取index對應下標的元素,再對index位置後的元素向前移動,最終返回被刪除的元素值。

轉換成陣列

  • public Object[] toArray():將ArrayList轉換為Object[]陣列;
  • public <T> T[] toArray(T[] a):將ArrayList轉換為指定T泛型的陣列;
/**
  * 將ArrayList轉換成Object型別陣列
  * @return Object[]
  */
@Override
public Object[] toArray() {
    // 返回的是Object[] 型別,需要注意;轉換成陣列就相當於是將 ArrayList的底層elementData暴露出去而已
    return Arrays.copyOf(elementData, size);
}


public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}
/**
  * 將ArrayList轉換成指定泛型的陣列
  * @param a
  * @return T[]
  */
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    // 《1》如果傳入的陣列小於 size 的大小,直接拷貝一個新的陣列返回
    if (a.length < size) {
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    }
    // 《2》否則直接拷貝陣列即可
    System.arraycopy(elementData, 0, a, 0, size);
    // 額 這個的目的是? 有點沒看懂
    if (a.length > size) {
        a[size] = null;
    }
    // 返回傳入的a,但是考慮到《1》的時候會返回新的陣列,所以即時《2》返回a陣列,最好還是按照 a = list.toArray(a); 來使用
    return a;
}

【注意】:我們在呼叫toArray()方法時,可能會遇到異常java.lang.ClassCastException:這是因為toArray()方法返回的型別是Object[],如果我們將其轉換為其他型別,可能排除異常。這是因為Java並不支援向下轉型,所以當我們需要將ArrayList轉換成陣列並且指定型別的時候,應該使用指定泛型的方法。

其他方法

/**
  * 判斷集合中是否含有元素 o
  * 可以看出,contains方法其實就是依賴的 indexOf方法
  * @param o
  * @return boolean
  */
@Override
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/**
  * 獲取指定index位的元素
  * @param index
  * @return E
  */
@Override
public E get(int index) {
    // 引數檢查
    rangeCheck(index);

    // 相當於直接返回 elementData[index]
    return elementData(index);
}

/**
  * 設定指定index位置的元素,並且返回該index位置舊元素值
  * 並返回舊元素的值(相當於替換)
  * @param index
  * @param element
  * @return E
  */
@Override
public E set(int index, E element) {
    // 引數檢查
    rangeCheck(index);
    
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}


/**
  * 清空集合
  */
@Override
public void clear() {
    // 陣列操作次數 + 1,雖然不知道記錄這個東東有啥子用
    modCount++;
    // 遍歷陣列,全部設定為null,同時更新size值
    for (int i = 0; i < size; i++) {
        elementData[i] = null;
    }

    size = 0;
}


 /**
   * 建立子陣列
   * 注意subList並不是只讀陣列,而是和父陣列共享相同的 elementData 的陣列,換句話說,對subList的操作會影響到 父陣列
   * 只不過是fromIndex 和 toIndex限制了檢視的範圍
   * @param fromIndex 開始下標
   * @param toIndex 結束下標
   *                是一個 [fromIndex, toIndex)的效果
   */
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

總結

本文從原始碼的角度,主要總結了ArrayList的屬性、構造方法、核心方法。有不準確之處,望指正!!!

相關文章