簡介
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
我的公衆號
歡迎關注我的公衆號,分享各種技術乾貨,各種學習資料,職業發展和行業動態。
技術交流羣
歡迎加入技術交流羣,來一起交流學習。