ArrayList是我們經常使用到的一個集合類,通過查看其底層源碼實現,有利於我們更好的掌握與使用。ArrayList不算多,但是也不算少,挨個看下去也挺費時費力的。所以呢,我打算循序漸進着來,比如這一篇就先看看ArrayList的屬性,以及一些常用的方法,例如增、刪、改、查。一口吃不成胖子,任重而道遠~
1、ArrayList的結構
下面是在IDEA裏查看到的ArrayList的UML圖:
從圖中我們可以看到:
- 總的來看,ArrayList 繼承了AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些接口;
- ArrayList 實現了RandmoAccess接口,即提供了隨機訪問功能。RandmoAccess是java中用來被List實現,爲List提供快速訪問功能的。這表明在ArrayList中,我們即可以通過元素的序號快速獲取元素對象(因爲底層實現是數組),這就是快速隨機訪問;
- ArrayList 實現了Cloneable接口,即覆蓋了函數clone(),能被克隆;
- ArrayList 實現java.io.Serializable接口,這意味着ArrayList支持序列化,能通過序列化去傳輸。
整體架構還是非常清晰的,那麼開始進入ArrayList的源碼開始探索內部原理。
2 、ArrayList的屬性分析
通過IDEA查看ArrayList的結構信息,圖中框出來的部分是它的屬性:
庖丁解牛,挑出重要的來一個個分析:
- private static final int DEFAULT_CAPACITY = 10:定義了ArrayList對象的默認容量爲10;
- private static final Object[] EMPTY_ELEMENTDATA = {} :定義了一個空的數組,在new ArrayList對象時如果參數爲0,那麼得到的這個空的數組;
- private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:同樣是定義了一個空的數組,不過和上邊的同,在new ArrayList對象時不傳參,那麼得到的這個空的數組;
- transient Object[] elementData:ArrayList中真正存儲元素的數組;
- private int size:用來表示數組中元素的個數。
這些字段單獨看來沒什麼意義,在使用過程中再細細體會其意義所在。
3、ArrayList的構造方法
通過IDEA查看類的結構信息,找出其中的構造方法:
可以看到ArrayList的構造方法共有三個重載,分別是:定義初始容量、默認無參構造和傳入一個Collection集合三種構造方法。
那麼接下來對這三個構造方法進行分析:
3.1 定義初始容量的構造方法
這種創建對象的方式很簡單,就是在new對象時給定一個初始值:
public static void main(String[] args) {
List list = new ArrayList(50); //初始容量爲50
}
那麼它是如何操作創建對象的囁?查看它的具體源代碼:(具體的分析將在註釋中寫出)
/**
* Constructs an empty list with the specified initial capacity.
* 構造具有指定初始容量的空列表。
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果給的初始值>0,創建一個大小爲initialCapacity的Object對象數組
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果給的初始值=0,則將EMPTY_ELEMENTDATA空數組賦給它
this.elementData = EMPTY_ELEMENTDATA;
} else {
//如果給的初始值<0,則拋出異常信息
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
3.2 默認的無參構造方法
這個方法就相對簡單了,看源代碼
/**
* Constructs an empty list with an initial capacity of ten.
* 構造一個初始容量爲10的空列表。
*/
public ArrayList() {
// 將默認的空的數組賦值給它
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
不是說構造一個初始容量爲10的列表嗎?10呢~?其實是當元素第一次被加入時,擴容至默認容量 10。先留個坑,後邊再說。
3.3 傳入一個Collection集合的構造方法
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
* 構造一個包含指定集合的元素的列表,按集合的迭代器返回元素的順序排列。
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
* 如果傳入的集合爲null,拋出空指針異常。
*/
public ArrayList(Collection<? extends E> c) {
// 先將傳入的集合轉化爲數組,然後ArrayList底層數組elementData指向該數組
elementData = c.toArray();
//將elementData中的元素個數賦值給sizes
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//保姆級別翻譯:c.toArray方法可能返回的不是一個Object數組,詳見6260652號BUG
if (elementData.getClass() != Object[].class)
//如果c.toArray() 返回的數組類型不是 Object[],則使用Arrays.copyOf()來構 //造一個大小爲 size的Object[]類型的數組給elementData數組
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
有個點我們需要注意一下,我們經常會通過size()方法查看一個集合中元素的個數,但是注意size和elementData.length是不相同的。size是指當前集合中存在的元素個數,elementData.length是指當前集合指定的容量大小。例如,如果在new ArrayList()時,ArrayList只是給我們默認的elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此時只是空數組,只有第一次調用add()時,纔將默認數組的大小設置爲爲DEFAULT_CAPACITY=10 ,此時elementData.length=10,list.size()=0。
小結一下:構造函數走完之後,會構建出數組elementData和數量size。
補充一點:關於方法Arrays.copyOf(elementData, size, Object[].class)
,就是根據class的類型來決定是new 還是反射去構造一個泛型數組,同時利用native函數,批量複製元素至新數組中。源代碼如下:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
//根據class的類型來決定是new 還是反射去構造一個泛型數組
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//利用native函數,批量賦值元素至新數組中。
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
4、add()方法源碼分析
構造完成ArrayList對象之後嘗試往集合里加入元素,那麼新創建的對象添加元素是什麼流程呢?在add()
方法處添加斷點,Debug走一下流程:
- 進入斷點內部,看它調用的方法:
/**
* Appends the specified element to the end of this list.
* 向列表末尾添加元素
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//這裏調用了ensureCapacityInternal方法,傳入的參數是size+1 *此時size = 0
//斷住,進去看代碼。
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- 方法內部調用了
ensureCapacityInternal
方法,傳的參數是數組和容量:
private void ensureCapacityInternal(int minCapacity) { //此時minCapacity=1
//方法內部調用了calculateCapacity
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
- 斷點進入
calculateCapacity
方法調用,繼續斷點進去看,這個就比較清晰了:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//利用判斷ArrayList對象是否是用默認構造函數初始化的
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//DEFAULT_CAPACITY = 10,minCapacity = 1,所以返回10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
- 該方法執行結束之後回到
ensureCapacityInternal
方法,繼續調用ensureExplicitCapacity
方法:
private void ensureCapacityInternal(int minCapacity) {
//繼而調用ensureExplicitCapacity方法,傳入的參數是 10
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
- 斷點進入
ensureExplicitCapacity
方法內部:(注意傳入的參數是10)
private void ensureExplicitCapacity(int minCapacity) {
//如果確定要擴容,會修改modCount
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0) //minCapacity = 10 length =0
grow(minCapacity); //調用grow方法
}
- 經過if判斷成立,繼續調用了grow(minCapacity)方法,傳的參數是 10,斷點進入:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 增加容量,以確保它至少可以容納由最小容量參數指定的元素數目。
* @param minCapacity the desired minimum capacity
*/
//這裏纔是真正的給容器擴容
private void grow(int minCapacity) {
//oldCapacity:舊的容器容量(0)
int oldCapacity = elementData.length;
//默認擴容一半:新的容器容量 = 舊容器的容量+舊容器的容量*0.5
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量小於minCapacity的話,就把minCapacity賦值給newCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//newCapacity 會與MAX_ARRAY_SIZE進行比較,不能超過Integer的最大值減8
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//將elementData指向一個新分配的數組,容量爲10
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看到這裏完成了容器(數組)的擴容操作,這段代碼執行完之後,會返回到:
這裏就將元素添加到了數組的0號位置,返回true
,表示添加成功,至此,add()
方法執行結束。
5、add(int index, E element) 方法源碼分析
此方法表示向list的指定位置添加元素,上代碼~:
public void add(int index, E element) { //傳入的參數分別是指定的位置,要添加的元素
rangeCheckForAdd(index); //越界判斷 如果越界拋異常
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //將index開始的數據 向後移動一位
elementData[index] = element; //加入新數據
size++; //元素個數+1
}
6、addAll(Collection<? extends E> c)方法源碼分析
此方法用於將一個集合裏的元素全部加入到當前集合中,代碼還算簡單:
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray(); //將結合轉換爲數組
int numNew = a.length; //獲得數組長度
ensureCapacityInternal(size + numNew); //判斷是否需要擴容
System.arraycopy(a, 0, elementData, size, numNew);//複製數組至elementData中
size += numNew; //計算當前容器中的元素個數
return numNew != 0;
}
7、addAll(int index, Collection<? extends E> c)方法源碼分析
此方法用於在指定位置插入整個集合:
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index); //越界判斷 如果越界拋異常
Object[] a = c.toArray(); //將結合轉換爲數組
int numNew = a.length; //獲得數組長度
ensureCapacityInternal(size + numNew); //判斷是否需要擴容
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);//移動(複製)數組
System.arraycopy(a, 0, elementData, index, numNew);//複製數組完成批量賦值
size += numNew;
return numNew != 0;
}