ArrayList是在Java中最常用的集合之一,其本質(zhì)上可以當(dāng)做是一個可擴容的數(shù)組,可以添加重復(fù)的數(shù)據(jù),也支持隨機訪問
ArrayList的類定義
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
其中,RandomAccess、Cloneable、Serializable三個接口都屬于標(biāo)記接口,沒有任何需要手動實現(xiàn)的方法
有人以為clone()方法來自于Cloneable接口,實際上clone()方法來自于Object類
- RandomAccess接口表示ArrayList支持隨機訪問,即可以直接訪問集合內(nèi)任意位置的元素
- Cloneable接口表示ArrayList支持克隆,并重寫了克隆方法
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
從上方代碼第四行出可以看出,調(diào)用clone()方法時,也重新創(chuàng)建了一個新的數(shù)組作為集合數(shù)據(jù)的容器,新舊兩個集合擁有各自的數(shù)據(jù)存儲的內(nèi)容空間,互不影響
- Serializable接口表示ArrayList可序列化
除此之外, 父類AbstractList也提供了部分通用方法的實現(xiàn)。但是這些方法有很多都進行了重寫,所以本文不會過多關(guān)注AbstractList類。但是它提供了一個非常重要的成員變量:modCount,表示集合長度被修改的次數(shù),在遍歷集合段落中會詳細(xì)講解它的作用
ArrayList 的成員變量分析
ArrayList的成員變量包含
// 序列化ID
private static final long serialVersionUID = 8683452581122892189L;
// 集合的默認(rèn)容量
private static final int DEFAULT_CAPACITY = 10;
// 用于初始化elementData的空數(shù)組
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默認(rèn)容量下用于初始化elementData的空數(shù)組
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存放實際元素的數(shù)組
transient Object[] elementData;
// 集合的大小
private int size;
// 集合的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
根據(jù)源碼,我們可以得到兩個比較直觀的信息:
- 集合的默認(rèn)容量是10,即調(diào)用無參構(gòu)造器生成的ArrayList,在不擴容的情況下,最多能夠存儲10個數(shù)據(jù)
- 集合的本質(zhì)是一個類型為Object的數(shù)組
要有一個值得注意的地方,成員變量中包含兩個用于初始化elementData的空數(shù)組:EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
在實例化ArrayList的時候,如果指定capacity(容量)為0,則使用EMPTY_ELEMENTDATA來初始化elementData;沒有指定capacity,也就是使用默認(rèn)容量capacity = 10的情況下,則使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA來初始化elementData
為什么要根據(jù)不同的情況使用不同的空接口。因為ArrayList要根據(jù)不同的情況進行擴容。通過new ArrayList(0)構(gòu)造的集合,其容量就是0;但是通過new ArrayList()構(gòu)造的集合,雖然初始化后elementData也是一個空數(shù)組,但是在添加第一個元素后,其會立即擴容為一個容量為10(默認(rèn)capacity)的數(shù)組。這是兩種不同的處理方式,所以要區(qū)分不同的空數(shù)組對象。在后續(xù)的構(gòu)造函數(shù)和添加元素段落中還會繼續(xù)講到
最后,為什么集合的最大容量是Integer.MAX_VALUE — 8?為什么要節(jié)省這8個容量?因為有一些虛擬機會在數(shù)組中保留一些頭信息或是描述信息,嘗試分配更大的數(shù)組可能會導(dǎo)致OutOfMemoryError
構(gòu)造器分析
ArrayList擁有三個構(gòu)造器,它們本質(zhì)上都是在對內(nèi)部的elementData進行初始化操作
- 無參構(gòu)造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
這是最簡單的構(gòu)造器,僅僅只是將elementData指向常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,以便于在添加第一個元素時,擴容至默認(rèn)容量capacity = 10的大小
- 顯式設(shè)置初始化容量的構(gòu)造器
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);
}
}
這個構(gòu)造器首先需要保證手動指定的容量大于0,否則會拋出IllegalArgumentException異常;其次,判斷手動指定的容量是否等于0,如果是,則直接將elementData指向靜態(tài)常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果不是,則創(chuàng)建一個跟指定容量相同大小的Object類型的數(shù)組作為elementData即可
- 基于另一個集合創(chuàng)建ArrayList的構(gòu)造器
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
這個構(gòu)造器相對比較復(fù)雜。首先,將參數(shù)集合轉(zhuǎn)換為數(shù)組對象。接下來,判斷新數(shù)組對象是否包含元素,對應(yīng)源碼的第3行。如果沒有元素,則直接將elementData指向靜態(tài)常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA。如果數(shù)組對象包含元素,則要判定數(shù)組對象的類型是否屬于Object數(shù)組類型(由于不同集合有不同的toArray方法的實現(xiàn),所以toArray方法不一定返回的是Object類型的數(shù)組)。在類型不屬于Object數(shù)組類型時,根據(jù)當(dāng)前的數(shù)組對象的大小和實際元素,復(fù)制一個新的Object類型的數(shù)組作為elementData
重要方法
添加元素
在開始分析添加元素的方法之前,我們需要先來分析幾個工具方法
- 確保集合內(nèi)容容量充足的前置方法
/**
* 確保集合內(nèi)容容量充足的前置方法
* @Param minCapacity 調(diào)用者指定的最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
在添加元素時,ArrayList會判斷當(dāng)前容量是否充足,在不充足的情況下進行擴容。ensureCapacityInternal(int)方法只是用來獲取到當(dāng)前所需要的最小容量。正如源碼所展示的,當(dāng)前所需要的容量并不一定是方法調(diào)用者傳入的minCapacity的值,因為當(dāng)集合通過無參構(gòu)造器創(chuàng)建時,即elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA時,它的容量至少要達到10(默認(rèn)容量的大小:DEFAULT_CAPACITY)。
拿到最小需要的容量后,判斷當(dāng)前集合是否滿足條件,到底需不需要擴容,則交給了ensureExplicitCapacity(int)方法來處理
- 判斷集合是否需要擴容的方法
/**
* 判斷當(dāng)前集合是否需要擴容的方法
* @Param minCapacity 實際需要的最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
之前提到了ArrayList從AbstractList父類中繼承了一個非常重要的字段:modCount,這個字段表示的是集合內(nèi)部數(shù)組對象長度可能被修改的次數(shù),在所有有可能改變數(shù)組對象長度的方法中,modCount都會進行自增操作,用于保證集合迭代時不會因為集合的長度發(fā)生改變而出現(xiàn)奇怪的錯誤,其具體的用法會在后續(xù)遍歷元素段落中詳細(xì)分析
只要實際需要的最小容量大于當(dāng)前集合的容量,就需要擴容。具體擴容的操作交給grow(int)來處理
- 對集合進行擴容的方法
/**
* 實際需要的最小容量
* @Param minCapacity 實際需要的最小容量
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
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);
}
對集合進行擴容,我們需要知道擴容后的容量是多少?雖然當(dāng)前需要的最小容量是minCapacity,但是如果minCapacity小于原始容量的1.5倍,則ArrayList認(rèn)為這個minCapacity的容量也是不安全的,很有可能會進行第二次擴容。為了減少擴容帶來的消耗,此時ArrayList認(rèn)為擴容后的容量應(yīng)該為原始容量的1.5倍
不管是選擇minCapacity還是原始容量的1.5倍作為擴容后的容量,都會存在一種特殊情況:擴容后的容量大于了集合允許的最大容量(MAX_ARRAY_SIZE),這種情況應(yīng)該怎么處理?
前文說到,集合的最大容量為Integer.MAX_VALUE — 8,這節(jié)省的8個容量是用于某些虛擬機在數(shù)組中保存一些頭信息或描述信息。如果集合的容量實在不夠,那只能把原本節(jié)省下來的8個容量拿來使用,這也意味著,在某些虛擬機環(huán)境下,這樣的操作會導(dǎo)致OutOfMemoryError錯誤。
這個操作是由hugeCapacity(int)方法來完成的,由于這個方法非常簡單,在這里就不對其做單獨的分析了
確定擴容后的容量后,根據(jù)這個容量創(chuàng)建一個新的數(shù)組,并把原始數(shù)據(jù)的數(shù)據(jù)拷貝過來,集合的擴容也就完成了
到這里可以看出,擴容本身就是一種低效率的操作(除了要開辟新的內(nèi)存空間外,還得把數(shù)據(jù)一個一個的復(fù)制到新的空間中),并且隨著原始數(shù)據(jù)增加,操作的速度也會越來越慢(copy10個數(shù)據(jù)絕對比copy1個數(shù)據(jù)慢),所以最好能在構(gòu)造ArrayList時就預(yù)估好容量的大小,避免擴容帶來的開銷
現(xiàn)在我們再來分析添加元素的方法就比較簡單了
// 在集合的末尾添加一個元素
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
// 在集合的指定位置,添加一個元素
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
不管使用的是哪個方法,都需要判斷集合是否能夠容納size + 1容量的數(shù)據(jù)。如果達不到要求就開始擴容。其次,我們發(fā)現(xiàn)在指定位置添加元素是一件非常麻煩的事情,因為從指定位置開始到集合末尾的數(shù)據(jù),都得往后移動一位。所以在使用ArrayList的時候,盡量不要在集合中的某一個位置添加元素。如果確實存在這種需求,那么可以考慮使用LinkedList而不是ArrayList(后續(xù)我們也會對LinkedList的源碼進行分析)
這里有個很簡單的方法rangeCheckForAdd(int),其用來判斷參數(shù)index是否合法,不對其做單獨的分析
獲取元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
判斷指定坐標(biāo)是否越界(依賴于非常簡單的rangeCheck(int)方法),然后直接從內(nèi)部數(shù)據(jù)中獲取數(shù)據(jù)
刪除元素
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null;
return oldValue;
}
刪除方法遇到了跟add(int, E)方法同樣的問題,即要把指定位置開始至整個集合末尾的所有元素向前移動一位
到此可以看出,對于ArrayList來說,添加元素、刪除元素都不是那么便捷的事情,ArrayList的優(yōu)勢還是在于能夠快速的訪問元素
由于現(xiàn)在較操作前少了一個元素,所以在移動元素之后需要把當(dāng)前集合的最后一位元素的值設(shè)置為null
還有一個刪除元素的方法
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;
}
這個方法本質(zhì)上也是查找到指定元素在集合中的位置,再根據(jù)這個位置使用fastRemove(int)方法來執(zhí)行刪除操作
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;
}
而fastRemove(int)方法的原理跟remove(int)方法是一致的
注意,刪除操作也會改變集合的長度,所以每個刪除方法中都有modCount++操作
遍歷元素
在JDK1.5之后,通常使用foreach循環(huán)(也叫增強for循環(huán))來對集合進行遍歷
List<E> list = new ArrayList<>();
for(E e: list) {
System.out.println(e);
}
foreach循環(huán)本質(zhì)上是在調(diào)用迭代器
List<E> list = new ArrayList<>();
Iterator<E> iter = list.iterator();
while(iter.hasNext()) {
System.out.println(iter.next());
}
ArrayList調(diào)用iterator()方法返還的是一個類型為ArrayList$Itr的迭代器。這個類是ArrayList中的一個私有內(nèi)部類
private class Itr implements Iterator<E> {
int cursor; // 下一個返回的元素的索引
int lastRet = -1; // 上一次返還的元素的索引,如果沒有則為-1
int expectedModCount = modCount; // 預(yù)期的集合長度被修改的次數(shù)
}
終于要到modCount的用法了
當(dāng)一個modCount為0(長度沒有被修改過)的ArrayList調(diào)用Iterator()方法后,我們會得到這樣的一個Itr對象
// 偽代碼
Itr: {
cursor: 0,
lastRet: -1,
expectedModCount: 0
}
此時我們可以調(diào)用hasNext()方法來判斷是否還有元素未訪問,即是否能夠繼續(xù)迭代
public boolean hasNext() {
return cursor != size;
}
如果上一次訪問的元素是集合的最后一個元素,即上一次訪問的元素索引為size - 1,那么就不能再繼續(xù)迭代了。這種情況下,因為cursor屬性代表下一次訪問元素的索引,所以可以說,cursor的值等于上一次訪問元素的索引 + 1,也就是說當(dāng)cursor = (size - 1) + 1時,就可以表示集合已經(jīng)迭代完畢。反過來,當(dāng)cursor != (size -1) + 1時,就表示集合還可以繼續(xù)迭代
當(dāng)我們通過hasNext()方法確定集合還能夠迭代時,可以通過next()方法取出迭代的元素的值
public E next() {
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];
}
除去驗證的代碼,重要的是,我們能夠看到,cursor始終指向下一次返回元素的索引,同時,lastRet指向當(dāng)前返還元素的索引
假設(shè),此時有另一個程序?qū)Ξ?dāng)前集合中的某一處進行了添加操作(調(diào)用了add(int, e)方法),如果對這樣的情況放任不管,那么迭代器迭代的數(shù)據(jù)肯定會有錯誤。所以,有了一個方法:checkForComodification()來確保迭代時,集合的長度沒有發(fā)生改變
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
此時,因為集合調(diào)用了add(int, e)方法,所以集合的屬性modCount = modCount + 1,而迭代器的屬性是expectedModCount是在迭代器實例化的時候就已經(jīng)創(chuàng)建好的,expectedModCount = 0,因此,modCount != expectedModCount,表示迭代器生成之后,集合發(fā)生了長度方面的改變,會影響集合的遍歷操作,拋出ConcurrentModificationException異常。從AbstractList處誕生的modCount屬性,一直到了這里才有了自己的用武之地
但是,有些時候,我們確實需要在遍歷的過程中操作數(shù)據(jù),比如說:遍歷某個集合,把符合某種條件的數(shù)據(jù)刪除掉。對于這樣的情況,迭代器提供了一個通過它自己來修改集合元素的方法:remove()
注意,迭代器只提供了對集合元素進行刪除操作的方法,沒有添加元素的方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 確保集合本身沒有被其他方式修改過
checkForComodification();
try {
// 刪除上一次返回的元素
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// modCount被修改了,expectedModCount也要跟著修改
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
值得注意的是,執(zhí)行刪除操作后,lastRet的值變?yōu)榱?1,所以,不能通過迭代器進行連續(xù)的刪除操作
List<E> list = new ArrayList<>();
Iterator<E> iter = list.iterator();
// 這是錯誤的做法
iter.next();
iter.remove();
// 連續(xù)刪除是不支持的
iter.remove();
// 這是正確的做法
iter.next();
iter.remove();
iter.next();
iter.remove();
ArrayList還有兩個獲取迭代器的方法:listIterator()和listIterator(int),這兩個方法返還的都是ArrayList$ListItr類型的迭代器
public ListIterator<E> listIterator() {
return new ListItr(0);
}
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
而ArrayList$ListItr類同樣也是ArrayList的私有內(nèi)部類
private class ListItr extends Itr implements ListIterator<E> {
// 省略了屬性和方法……
}
ListItr繼承自Itr,是Itr的擴展,實現(xiàn)了從后往前遍歷、添加元素、修改元素、返還索引等方法,其思路大致與Itr相同,因此不在此詳細(xì)分析了
其它方法
ArrayList還有幾個常用且有效的方法值得分析
- 修改指定位置元素的值得方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
該方法修改指定位置元素的值,并把修改前的值返回。注意,修改方法并沒有modCount++的操作,因為修改集合內(nèi)某一元素的值不會對集合的長度發(fā)生影響,也就影響不到迭代器的操作了
- 清理集合內(nèi)剩余空間的方法
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
有時候,我們確定集合內(nèi)的元素不會再發(fā)生變化,而集合還有剩余容量時可以調(diào)用這個方法來清理剩余容量。分為兩種情況:
- 當(dāng)集合的尺寸為0的時候,elementData指向常量** EMPTY_ELEMENTDATA**
- 當(dāng)集合的尺寸不為0的時候,根據(jù)當(dāng)前尺寸創(chuàng)建一個新的數(shù)組,復(fù)制原數(shù)組數(shù)據(jù),將新的數(shù)組賦值給elementData
注意,這也是一個可能會影響集合長度的方法 ,所以也有modCount++操作
- 手動擴容的方法
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
有些時候我們會在集合實例化之后才確認(rèn)到具體的容量,而按照默認(rèn)的擴容方式(每次擴容50%),可能需要多次擴容才能達到預(yù)期的容量。前文說過,擴容是一個低效率的操作,為了避免多次執(zhí)行這樣的操作,所以我們可以主動調(diào)用ensureCapacity(int)方法來進行擴容
參數(shù)minCapacity代表預(yù)期的容量。但是對于通過無參構(gòu)造器創(chuàng)建的ArrayList(也就是說其容量為10),如果minCapacity小于10,則ArrayList依然認(rèn)為自己的容量應(yīng)該是10,不會進行任何操作
小結(jié)
本文較為詳細(xì)的分析了ArrayList的源碼,也提出了一些使用ArrayList的技巧,希望能夠?qū)Ω魑婚_發(fā)者帶來幫助。下一步我會繼續(xù)分享LinkedList的源碼分析,請大家多多支持