面試官:你說你精通原始碼,那你知道ArrayList 原始碼的設計思路嗎?
Arraylist原始碼分析
ArrayList 我們幾乎每天都會使用到,但是通常情況下我們只是知道如何去使用,至於其內部是怎麼實現的我們不關心,但是有些時候面試官就喜歡問與ArrayList 的原始碼相關的問題,今天我們就來看看和ArrayList 原始碼相關的問題。
一:整體架構
1.1、ArrayList 結構
ArrayList 整體架構比較簡單,就是一個數組結構,比較簡單,如下圖:
圖中展示是長度為 n 的陣列,index 表示陣列的下標,從 0 開始計數,elementData 表示陣列本身,原始碼中除了這兩個概念,還有以下三個基本概念:
- DEFAULT_CAPACITY ,表示陣列的初始大小,預設是 10;
- size ,當前陣列的大小,沒有使用 volatile 修飾,非執行緒安全的;
- modCount ,統計當前陣列被修改的版本次數;
1.2、ArrayList 類註釋
看原始碼,首先要看類註釋,我們看看類註釋上面都說了什麼,部分截圖如下圖所示:
類註釋主要講了以下四點
- 允許put null 值,會自動擴容 ;
- size、isEmpty、get、set、add 等方法時間複雜度都是 O (1) ;
- 是非執行緒安全的,多執行緒情況下,推薦使用執行緒安全類:Collections#synchronizedList ;
- 增強 for 迴圈,或者使用迭代器迭代過程中,如果陣列大小被改變,會快速失敗,丟擲異常
二:原始碼解析
2.1、初始化
ArrayList 有三種初始化辦法:無引數直接初始化、指定大小初始化、指定初始資料初始化,原始碼如下:
// 無引數直接初始化,陣列大小為空 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 指定大小初始化 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(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) { elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
注意: ArrayList 無參構造器初始化時,預設大小是空陣列,並不是大家常說的 10,10 是在第一次 add 的時候擴容的陣列值。
2.2、新增和擴容實現
新增方法主要分成兩步:首先判斷陣列是否需要擴容,如果需要,執行擴容操作,否則,直接賦值。新增方法原始碼如下,其中ensureCapacityInternal()方法就是擴容操作。
public boolean add(E e) {
//確保陣列大小是否足夠,不夠執行擴容,size 為當前陣列的大小
ensureCapacityInternal(size + 1); // Increments modCount!!
//直接賦值,執行緒不安全的
elementData[size++] = e;
return true;
}
擴容(ensureCapacityInternal)的原始碼:
private void ensureCapacityInternal(int minCapacity) {
// 如果初始化陣列大小時,有給定初始值,以給定的大小為準,不走 if 邏輯
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 確保容積足夠
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 記錄陣列被修改
modCount++;
// 如果我們期望的最小容量大於目前陣列的長度,那麼就擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 擴容,並把現有資料拷貝到新的數組裡面去
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果擴容後的值 < 我們的期望值,擴容後的值就等於我們的期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果擴容後的值 > jvm 所能分配的陣列的最大值,那麼就用 Integer 的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 通過複製進行擴容
elementData = Arrays.copyOf(elementData, newCapacity);
}
從擴容的原始碼我們可以看出:
- 擴容的規則並不是翻倍,而是原來容量的 1.5 倍 ;
- ArrayList 中的陣列容量的最大值是 Integer.MAX_VALUE,超過這個值,JVM 就不會給陣列分配記憶體空間了 ;
2.3、刪除
ArrayList 刪除元素有很多種方式,比如根據陣列索引刪除、根據值刪除或批量刪除等等,原理和思路都差不多,我們選取根據值刪除方式來進行原始碼說明:
public boolean remove(Object o) {
// 如果要刪除的值是 null,找到第一個值是 null 的刪除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 如果要刪除的值不為 null,找到第一個和要刪除的值相等的刪除
for (int index = 0; index < size; index++)
// 這裡是根據 equals 來判斷值相等的,相等後再根據索引位置進行刪除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
從上面的原始碼中我們可以看出:
- 由於新增的時候沒有對 null 值做出判斷,所以是可以刪除 null 值的 ;
- 找到值在陣列中的索引位置,是通過 equals 來判斷的,如果陣列元素不是基本型別,我們需要關注 equals 的具體實現;
刪除的具體邏輯是在fastRemove()中實現的,原始碼如下所示
private void fastRemove(int index) {
// 記錄陣列的結構要發生變動了
modCount++;
// numMoved 表示刪除 index 位置的元素後,需要從 index 後移動多少個元素到前面去
// 減 1 的原因,是因為 size 從 1 開始算起,index 從 0開始算起
int numMoved = size - index - 1;
if (numMoved > 0)
// 從 index +1 位置開始被拷貝,拷貝的起始位置是 index,長度是 numMoved
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//陣列最後一個位置賦值 null,幫助 GC
elementData[--size] = null;
}
2.4、複雜度分析
操作 | 時間複雜度 |
---|---|
get() | 根據下標直接查詢 O(1) |
add(E e) | 直接尾部新增 O(1) |
add(int index, E e) | 插入後元素需要往後移動一個單位 O(n) |
remove(E e) | 刪除後元素需要往前移動一個單位 O(n) |
2.5、執行緒安全
我們需要強調的是,只有當 ArrayList 作為共享變數時,才會有執行緒安全問題,當 ArrayList 是方法內的區域性變數時,是沒有執行緒安全的問題的。
ArrayList 有執行緒安全問題的本質,是因為 ArrayList 自身的 elementData、size、modConut 在進行各種操作時,都沒有加鎖,而且這些變數的型別並非是可見(volatile)的,所以如果多個執行緒對這些變數進行操作時,可能會有值被覆蓋的情況。
類註釋中推薦我們使用 Collections#synchronizedList 來保證執行緒安全,SynchronizedList 是通過在每個方法上面加上鎖來實現,雖然實現了執行緒安全,但是效能大大降低。
三:總結
從上面 ArrayList 的部分原始碼的分析中,我們可以發現 ArrayList 底層其實就是圍繞陣列結構,各個 API 都是對陣列的操作進行封裝,讓使用者無需感知底層實現,只需關注如何使用即可。
最後
感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!