簡介
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
我的公眾號
歡迎你「掃一掃」下面的二維碼,關注我的公眾號,可以接受最新的文章推送,有豐厚的抽獎活動和福利等著你哦!?
如果你有什麼疑問或者問題,可以 點選這裡 提交 issue,也可以發郵件給我 jeanboy@foxmail.com。
來一起交流學習,群裡有很多大牛和學習資料,相信一定能幫助到你!