原來 ArrayList 內部原理這麼簡單

jeanboy發表於2019-03-08

簡介

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波斯灣

如果你有什麼疑問或者問題,可以 點選這裡 提交 issue,也可以發郵件給我 jeanboy@foxmail.com

同時歡迎你

Android技術進階:386463747
來一起交流學習,群裡有很多大牛和學習資料,相信一定能幫助到你!

相關文章