現在由大惡人付有傑來從增刪改查幾個角度輕度解析ArrayList的源碼
首先ArrayList的底層數據結構非常簡單,就是一個數組。
從源碼第115行我們可以得出信息,他的默認數組長度是10。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
那麼我們經常調用的size方法是什麼呢?
源碼第281行,142行
/**
* Returns the number of elements in this list.
*返回鏈表中元素的個數
* @return the number of elements in this list
*/
public int size() {
return size;
}
/**
* The size of the ArrayList (the number of elements it contains).
*同上
* @serial
*/
private int size;
另外,還有一個關鍵的屬性:
//modCount 統計當前數組被修改的版本次數,數組結構有變動,就會 +1
protected transient int modCount = 0;
以上表達的意思就是說,ArrayList的默認大小是10,內部記錄有自己被修改的次數,和鏈表中有效的元素。所謂有效的元素就是你自己添加的元素。
1.構造方法
有三種構造方法:
1.指定大小初始化
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);
}
}
清晰明瞭哈,如果指定的大小是大於0的,那麼就用這個數字初始化,否則就初始一個空的數組。如果是非法輸入(<0),就會拋出IllegalArgumentException
異常。
2.無參構造函數初始化
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
我們可以看到,無參構造 函數並不是一來就 初始化了10個長的數組,而是初始化了一個空的數組。這樣能夠省點空間吧。面試官問起來了,初始化的大小是10碼?絕對不是哈,是0。
3.指定數據初始化
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;
}
}
這個代碼的意思就是,凡是繼承於Collection
的,爺都能初始化。List接口繼承自Collection
的。
演示一下2種姿勢
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
ArrayList<Integer> arrayList = new ArrayList<>(list);
ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(2, 4, 5, 6, 7, 8));
是不是很方便,如果你不知道這個方法,你還要手動去add 1 2 3 4 5.
2.新增和擴容實現
新增就是往數組中添加元素,主要分成兩步:
- 判斷是否需要擴容,如果需要執行擴容操作;
- 直接賦值。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
//判斷索引是不是合法的
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
我們常用時第一種,第二種是在指定位置添加,把原來位置 的擠到後面去。後面的所有元素都要讓一步,性能消耗會很大。
在添加元素之前,總是有一個:
//確保數組大小是否足夠,不夠執行擴容,size 爲當前數組的大小
ensureCapacityInternal(size + 1);
我們仔細想一想是吧,你添加元素,size就加1,所以就判斷size+1是否滿足。
在ensureCapacityInternal(size + 1);
做了很多事情。我把相關的代碼都複製過來。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, 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;
//原來老舊的容量除以2,加上老的容量。實錘了!!1.5倍速擴容
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果擴容了,還是不夠用,那麼就用你聲明的值。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果擴容了,大於Integer.MaxValue 就用Inter.maxValue
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//將數據從原來的數組複製到新的數組上
elementData = Arrays.copyOf(elementData, newCapacity);
}
上面代碼,關鍵地方我都給出了中文解釋。希望你能明白。
所以我們可以總結一下:
- 擴容的規則並不是翻倍,是原來容量大小 + 容量大小的一半,直白來說,擴容後的大小是原來容量的 1.5 倍;
int newCapacity = oldCapacity + (oldCapacity >> 1);
- ArrayList 中的數組的最大值是 Integer.MAX_VALUE,超過這個值,JVM 就不會給數組分配內存空間了。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
- 新增時,並沒有對值進行嚴格的校驗,所以 ArrayList 是允許 null 值的。
- 源碼在擴容的時候,有數組大小溢出意識,就是說擴容後數組的大小下界不能小於 0,上界不能大於 Integer
的最大值,這種意識我們可以學習。
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
擴容的本質:
擴容的本質就是新開了一個擴容的數組,然後把原來數組的元素批量賦值過去,最後修改內部的elementData引用。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//複製了原有 數組元素,然後修改elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
** Arrays.copyOf是調用的 System.arraycopy,後者是本地方法**
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
3.刪除
ArrayList 刪除元素有很多種方式,比如根據數組索引刪除、根據值刪除或批量刪除等等,原理和思路都差不多,我們選取根據值刪除方式來進行源碼說明:
代碼532行:
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(源碼544行)
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;
}
從源碼中,我們可以看出,某一個元素被刪除後,爲了維護數組結構,我們都會把數組後面的元素往前移動(所以說,數組的刪除性能開銷真的很大)
4.迭代
如果要自己實現迭代器,實現 java.util.Iterator 類就好了,ArrayList 也是這樣做的(內部類的方式),我們來看下迭代器的幾個總要的參數:
int cursor;// 迭代過程中,下一個元素的位置,默認從 0 開始。
int lastRet = -1; // 新增場景:表示上一次迭代過程中,索引的位置;刪除場景:爲 -1。
int expectedModCount = modCount;// expectedModCount 表示迭代過程中,期望的版本號;modCount 表示數組實際的版本號。
迭代器一般來說有三個方法:
- hasNext 還有沒有值可以迭代
- next 如果有值可以迭代,迭代的值是多少
- remove 刪除當前迭代的值
public boolean hasNext() {
return cursor != size;//cursor 表示下一個元素的位置,size 表示實際大小,如果兩者相等,說明已經沒有元素可以迭代了,如果不等,說明還可以迭代
}
public E next() {
//迭代過程中,判斷版本號有無被修改,有被修改,拋 ConcurrentModificationException 異常
checkForComodification();
//本次迭代過程中,元素的索引位置
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 下一次迭代時,元素的位置,爲下一次迭代做準備
cursor = i + 1;
// 返回元素值
return (E) elementData[lastRet = i];
}
// 版本號比較
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
從源碼中可以看到,next 方法就幹了兩件事情,第一是檢驗能不能繼續迭代,第二是找到迭代的值,併爲下一次迭代做準備(cursor+1)。
public void remove() {
// 如果上一次操作時,數組的位置已經小於 0 了,說明數組已經被刪除完了
if (lastRet < 0)
throw new IllegalStateException();
//迭代過程中,判斷版本號有無被修改,有被修改,拋 ConcurrentModificationException 異常
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
// -1 表示元素已經被刪除,這裏也防止重複刪除
lastRet = -1;
// 刪除元素時 modCount 的值已經發生變化,在此賦值給 expectedModCount
// 這樣下次迭代時,兩者的值是一致的了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
- 刪除元素成功,數組當前 modCount 就會發生變化,這裏會把 expectedModCount 重新賦值,下次迭代時兩者的值就會一致了
其他:
都說數組的添加元素 的時間複雜度是O(1),真的如此嗎?
如果我們直接調用Add(x)的方法,且數組容量足夠這個數組掛在後面,那麼時間複雜度就是1,如果觸發了擴容機制,那麼就是O(N),在使用add(index,e)的時候,時間複雜度一般來說不是O1,因爲要移動索引 後面的元素。
什麼是falilFast的機制?
在遍歷過程中,如果數據被修改,就會報錯。
Iterator<Integer> iterator = arrayList.iterator();
new Thread(new Runnable() {
@Override
public void run() {
arrayList.remove(2);
}
}).start();
while (iterator.hasNext()){
Thread.sleep(50);
System.out.println(iterator.next());
}
//同上
for (Integer integer : arrayList) {
System.out.println(integer);
}
上面提到,遍歷的時候會記錄modCount的值,如果和自己期望的不一樣,就會報錯。
fail-fast解決辦法
方案一:在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,這樣就可以解決。但是不推薦,因爲增刪造成的同步鎖可能會阻塞遍歷操作。
方案二:使用CopyOnWriteArrayList來替換ArrayList。推薦使用該方案。
數組初始化,被加入一個值後,如果我使用 addAll 方法,一下子加入 15 個值,那麼最終數組的大小是多少?
分析:在加入一個元素的時候,數組被初始化成10個,然後一下子加入15個,那麼就會觸發擴容,在初次擴容後,大小變成了15(1.5倍速度擴容),發現 還是不夠用,就會使用1+15這個值作爲容量。所以答案是16.
再貼一遍源碼
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//原來老舊的容量除以2,加上老的容量。實錘了!!1.5倍速擴容
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果擴容了,還是不夠用,那麼就用你聲明的值。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果擴容了,大於Integer.MaxValue 就用Inter.maxValue
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//將數據從原來的數組複製到新的數組上
elementData = Arrays.copyOf(elementData, newCapacity);
}
現在我有一個很大的數組需要拷貝,原數組大小是 5k,請問如何快速拷貝?
因爲原數組比較大,如果新建新數組的時候,不指定數組大小的話,就會頻繁擴容,頻繁擴容就會有大量拷貝的工作,造成拷貝的性能低下,所以回答說新建數組時,指定新數組的大小爲 5k 即可。
所以大惡人付有傑建議,平常自己心知肚明的時候,自己手動指定大小。
還有ArrayList刪不乾淨的問題:
List<Integer> list = new ArrayList<>(Arrays.asList(1,2,2,2,2,3));
for(int i = 0;i<list.size();i++){
if(list.get(i).equals(2)){
list.remove(i);
}
}
System.out.println(list.toString());
代碼輸出:
兄弟們自己去想吧。