一、前言
在前面幾篇,我們已經學習了常見了Map,下面開始閱讀實現Collection接口的常見的實現類。在有了之前源碼的鋪墊之后,我們后面的閱讀之路將會變得簡單很多,因為很多Collection的結構與Map的類似,甚至有不少是直接用了Map里的方法。接下來讓我們一起來看一下ArrayList
的源碼。
二、ArrayList結構概覽

顧名思義,ArrayList的結構實際就是一個Object[]
。所以它的特性很明顯,插入一個元素的時候,是耗時是一個常量時間O(1),在插入n個元素的時候,需要的時間就是O(n)。其他的操作中,運行的時間也是一個線性的增長(與數組中的元素個數有關)。
三、ArrayList源碼閱讀
3.1 ArrayList的繼承關系
其中值得一提的是RandomAccess
接口,該接口的目的是這么說的:
List
實現所使用的標記接口,用來表明其支持快速(通常是固定時間)隨機訪問。此接口的主要目的是允許一般的算法更改其行為,從而在將其應用到隨機或連續訪問列表時能提供良好的性能。
對於順序訪問的list,比如LinkedList,使用Iterator訪問會比使用for-i來遍歷list更快。這一點其實很好理解,當對於LinkedList使用get(i)的時候,由於是鏈表結構,所以每次都會從表頭開始向下搜索,耗時肯定會多。
對於實現RandomAccess這個接口的類,如ArrayList,我們在遍歷的時候,使用for(int i = 0; i < size; i++)
來遍歷,其速度比使用Iterator快(接口上是這么寫的)。但是筆者看源碼的時候,Iterator里使用的也是i++
,這種遍歷,無非是增加了fail-fast判斷,估計就是這個導致了性能的差距,但是沒有LinkedList這么大。筆者循環了 1000 * 1000 次,貼出比較結果,僅供參考,有興趣的朋友們可以試一試,循環次數越多越明顯:
----------now is arraylist----------
使用Iterator迭代一共花了19ms時間
使用for-i迭代一共花了9ms時間
----------now is linkedList----------
使用Iterator迭代一共花了17ms時間
使用for-i迭代一共花了434ms時間
而其繼承的AbstractList主要給ArrayList提供了諸如add
,get
,set
,remove
的集合方法。
3.2 ArrayList的成員變量
//初始化默認容量
private static final int DEFAULT_CAPACITY = 10;
// 空對象數組
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默認容量的空對象數組
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 實際存儲對象的數組
transient Object[] elementData;
// 存儲的數量
private int size;
// 數組能申請的最大數量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
特別說明一下: EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA是為了在調用構造方法的時候,給elementData數組初始化,當elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的時候,當ArrayList第一次插入元素的時候,它的數組大小將會被初始化為DEFAULT_CAPACITY。而EMPTY_ELEMENTDATA可以理解為初始化的時候size=0,下面讓我們看下構造方法,來更加清楚的理解。
3.3 ArrayList的構造方法
3.3.1 ArrayList()
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
當調用默認構造函數的時候,給elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
3.3.2 ArrayList(int initialCapacity)
public ArrayList(int initialCapacity) {
// 當 initialCapacity > 0 時,初始化對應大小的數組
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
// 為 0 時,用指向EMPTY_ELEMENTDATA
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
這里當initialCapacity=0的時候,就是上述提到的情況。
3.3.3 ArrayList(Collection<? extends E> c)
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray不返回Object[]的時候,則進行數組拷貝
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果為空,則指向EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
}
3.4 ArrayList的重要方法
3.4.1 get(int index)
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
get方法很簡單,就是先檢查index范圍是否正確,正確的話從數組里取出元素。
private void rangeCheck(int index) {
// 如果index 大於 存儲的個數,則拋出異常
if (index >= size)
// outOfBoundsMsg里面就是簡單的字符串拼接。
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
這里值得一提的是:這里只判斷了index >= size
的情況,對於index < 0
的情況沒有判斷,是因為在獲取數組值的時候,如果為負數會拋出ArrayIndexOutOfBoundsException異常。
3.4.2 add(E e)
在看源碼之前,我們先思考一個問題,往數組里添加元素的時候要注意什么:
- 對於剛初始化的數組,要初始化它的大小
- 判斷數組大小是否足夠,如果不夠大,擴容
- 對於擴容要判斷是否到達數組的最大數量
知道這些需要考慮之后,我們再來看看它的代碼:
public boolean add(E e) {
//對上述的3個前提進行判斷
ensureCapacityInternal(size + 1);
//賦值,然后指針走到下一個空位
elementData[size++] = e;
return true;
}
我們接着來看ensureCapacityInternal()的方法代碼:
private void ensureCapacityInternal(int minCapacity) {
// 上述情況一:初始化數組的大小
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 取minCapacity和DEFAULT_CAPACITY中較大的那個
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 檢查有沒有擴容的必要
ensureExplicitCapacity(minCapacity);
}
ensureCapacityInternal()方法的作用就是對構造方法初始化的數組進行處理。
再來看一下ensureExplicitCapacity():
private void ensureExplicitCapacity(int minCapacity) {
// 修改次數的計數器(在AbstractList中定義的)
modCount++;
// 如果需要的空間大小 > 當前數組的長度,則進行擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
ensureExplicitCapacity()檢查是否需要擴容。
private void grow(int minCapacity) {
// 記錄舊的length
int oldCapacity = elementData.length;
// 擴容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)
throw new OutOfMemoryError();
// 需要的最小容量 > 數組最大的長度,則取Integer的最大值,否則取數組最大長度
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
最后的grow()擴容就是判斷有沒有超過數組的最大長度,以及對應的處理。
3.4.3 remove(int index)
public E remove(int index) {
rangeCheck(index);
// 修改計數器
modCount++;
// 記錄舊值,返回
E oldValue = elementData(index);
// 計算要往前移動的元素個數
int numMoved = size - index - 1;
//個數大於0,進行拷貝,從index+1開始,拷貝numMoved個,拷貝起始位置是index
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 設置為null,以便GC
elementData[--size] = null;
return oldValue;
}
對於被刪除的元素,其后面的元素需要往前移。
3.4.4 remove(Object o)
public boolean remove(Object o) {
// 判斷o為null,loop遍歷找到為null的元素
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
// 不為null
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 與上面的remove(int index) 類似
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;
}
3.4.5 set(int index, E element)
public E set(int index, E element) {
rangeCheck(index);
// 記錄舊的值
E oldValue = elementData(index);
//在原位置設置新的值
elementData[index] = element;
return oldValue;
}
設置index位置的元素值為element,返回該位置的原來的值
3.4.6 addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
// 對於新的最小長度進行判斷處理
ensureCapacityInternal(size + numNew);
//將a數組,從index-0開始,拷貝numNew個,到elementData的size位置
System.arraycopy(a, 0, elementData, size, numNew);
//將size增加numNew個
size += numNew;
return numNew != 0;
}
四、總結
ArrayList在隨機訪問的時候,數組的結構導致訪問效率比較高,但是在指定位置插入,以及刪除的時候,需要移動大量的元素,導致效率低下,在使用的時候要根據場景特點來選擇,另外注意循環訪問的方式選擇。最后謝謝各位園友觀看,如果有描述不對的地方歡迎指正,與大家共同進步!