原來 ArrayList 內部原理這麼簡單

簡介

ArrayList 是一種變長的基於數組實現的集合類,ArrayList 允許空值和重複元素,當往 ArrayList 中添加的元素數量大於其底層數組容量時,它會自動擴容至一個更大的數組。

另外,由於 ArrayList 底層基於數組實現,所以其可以保證在 O(1) 複雜度下完成隨機查找操作。其他方面,ArrayList 是非線程安全類,併發環境下,多個線程同時操作 ArrayList,會引發不可預知的錯誤。

ArrayList 是大家最爲常用的集合類,我們先來看下常用的方法:

List<String> dataList = new ArrayList<>();//創建 ArrayList
dataList.add("test");//添加數據
dataList.add(1,"test1");//指定位置,添加數據
dataList.get(0);//獲取指定位置的數據
dataList.remove(0);//移除指定位置的數據
dataList.clear();//清空數據

構造方法

ArrayList 有兩個構造方法,一個是無參,另一個需傳入初始容量值。大家平時最常用的是無參構造方法,相關代碼如下:

private static final int DEFAULT_CAPACITY = 10; // 初始容量爲 10
private static final Object[] EMPTY_ELEMENTDATA = {};// 一個空對象
// 一個空對象,如果使用默認構造函數創建,則默認對象內容默認是該值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; //當前數據對象存放地方,當前對象不參與序列化
private int size; // 當前數組長度

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

上面的代碼比較簡單,兩個構造方法做的事情並不複雜,目的都是初始化底層數組 elementData。區別在於無參構造方法會將 elementData 初始化一個空數組,插入元素時,擴容將會按默認值重新初始化數組。而有參的構造方法則會將 elementData 初始化爲參數值大小(>= 0)的數組。

add()

對於數組(線性表)結構,插入操作分爲兩種情況。一種是在元素序列尾部插入,另一種是在元素序列其他位置插入。

  • 尾部插入元素
/** 在元素序列尾部插入 */
public boolean add(E e) {
    // 1. 檢測是否需要擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 2. 將新元素插入序列尾部
    elementData[size++] = e;
    return true;
}

對於在元素序列尾部插入,這種情況比較簡單,只需兩個步驟即可:

  1. 檢測數組是否有足夠的空間插入
  2. 將新元素插入至序列尾部

如下圖:

img

  • 指定位置插入元素
/** 在元素序列 index 位置處插入 */
public void add(int index, E element) {
    if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    // 1. 檢測是否需要擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 2. 將 index 及其之後的所有元素都向後移一位
    // arraycopy(被複制的數組, 從第幾個元素開始, 複製到哪裏, 從第幾個元素開始粘貼, 複製的元素個數)
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 3. 將新元素插入至 index 處
    elementData[index] = element;
    size++;
}

如果是在元素序列指定位置(假設該位置合理)插入,則情況稍微複雜一點,需要三個步驟:

  1. 檢測數組是否有足夠的空間
  2. 將 index 及其之後的所有元素向後移一位
  3. 將新元素插入至 index 處

如下圖:

img

從上圖可以看出,將新元素插入至序列指定位置,需要先將該位置及其之後的元素都向後移動一位,爲新元素騰出位置。這個操作的時間複雜度爲O(N),頻繁移動元素可能會導致效率問題,特別是集合中元素數量較多時。在日常開發中,若非所需,我們應當儘量避免在大集合中調用第二個插入方法。

擴容機制

下面就來簡單分析一下 ArrayList 的擴容機制,對於變長數據結構,當結構中沒有空餘空間可供使用時,就需要進行擴容。在 ArrayList 中,當空間用完,其會按照原數組空間的 1.5 倍進行擴容。相關源碼如下:

/** 計算最小容量 */
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;
    // newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 進行擴容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 如果最小容量超過 MAX_ARRAY_SIZE,則將數組容量擴容至 Integer.MAX_VALUE
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

上面就是擴容的邏輯,邏輯很簡單,這裏就不贅述了。

get()

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    return (E) elementData[index];
}

get 的邏輯很簡單,就是檢查是否越界,根據 index 獲取元素。

remove()

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    modCount++;
    // 返回被刪除的元素值
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 將 index + 1 及之後的元素向前移動一位,覆蓋被刪除值
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 將最後一個元素置空,並將 size 值減 1     
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

E elementData(int index) {
    return (E) elementData[index];
}

/** 刪除指定元素,若元素重複,則只刪除下標最小的元素 */
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 遍歷數組,查找要刪除元素的位置
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/** 快速刪除,不做邊界檢查,也不返回刪除的元素值 */
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; // clear to let GC do its work
}

上面的刪除方法並不複雜,這裏以第一個刪除方法爲例,刪除一個元素步驟如下:

  1. 獲取指定位置 index 處的元素值
  2. 將 index + 1 及之後的元素向前移動一位
  3. 將最後一個元素置空,並將 size 值減 1
  4. 返回被刪除值,完成刪除操作

如下圖:

img

上面就是刪除指定位置元素的分析,並不是很複雜。

現在,考慮這樣一種情況。我們往 ArrayList 插入大量元素後,又刪除很多元素,此時底層數組會空閒處大量的空間。因爲 ArrayList 沒有自動縮容機制,導致底層數組大量的空閒空間不能被釋放,造成浪費。對於這種情況,ArrayList 也提供了相應的處理方法,如下:

/** 將數組容量縮小至元素數量 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

通過上面的方法,我們可以手動觸發 ArrayList 的縮容機制。這樣就可以釋放多餘的空間,提高空間利用率。

img

clear()

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

clear 的邏輯很簡單,就是遍歷一下將所有的元素設置爲空。

我的 GitHub

github.com/jeanboydev

我的公衆號

歡迎關注我的公衆號,分享各種技術乾貨,各種學習資料,職業發展和行業動態。

Android 波斯灣

技術交流羣

歡迎加入技術交流羣,來一起交流學習。

QQ 技術交流羣
微信技術交流羣
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章