用了這麼久 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; } 複製程式碼
對於在元素序列尾部插入,這種情況比較簡單,只需兩個步驟即可:
- 檢測陣列是否有足夠的空間插入
- 將新元素插入至序列尾部
如下圖:

- 指定位置插入元素
/** 在元素序列 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++; } 複製程式碼
如果是在元素序列指定位置(假設該位置合理)插入,則情況稍微複雜一點,需要三個步驟:
- 檢測陣列是否有足夠的空間
- 將 index 及其之後的所有元素向後移一位
- 將新元素插入至 index 處
如下圖:

從上圖可以看出,將新元素插入至序列指定位置,需要先將該位置及其之後的元素都向後移動一位,為新元素騰出位置。這個操作的時間複雜度為 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 } 複製程式碼
上面的刪除方法並不複雜,這裡以第一個刪除方法為例,刪除一個元素步驟如下:
- 獲取指定位置 index 處的元素值
- 將 index + 1 及之後的元素向前移動一位
- 將最後一個元素置空,並將 size 值減 1
- 返回被刪除值,完成刪除操作
如下圖:

上面就是刪除指定位置元素的分析,並不是很複雜。
現在,考慮這樣一種情況。我們往 ArrayList 插入大量元素後,又刪除很多元素,此時底層陣列會空閒處大量的空間。因為 ArrayList 沒有自動縮容機制,導致底層陣列大量的空閒空間不能被釋放,造成浪費。對於這種情況,ArrayList 也提供了相應的處理方法,如下:
/** 將陣列容量縮小至元素數量 */ public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } } 複製程式碼
通過上面的方法,我們可以手動觸發 ArrayList 的縮容機制。這樣就可以釋放多餘的空間,提高空間利用率。

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
https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode
我的公眾號
歡迎你「掃一掃」下面的二維碼,關注我的公眾號,可以接受最新的文章推送,有豐厚的抽獎活動和福利等著你哦!:heart_eyes:

如果你有什麼疑問或者問題,可以 點選這裡 提交 issue,也可以發郵件給我[email protected]。
同時歡迎你

來一起交流學習,群裡有很多大牛和學習資料,相信一定能幫助到你!