推薦閱讀時間 13min+
目錄
- ArrayList 關鍵詞
- 源碼閱讀
- 問題解答和總結
前言
本文基于Java8的源代碼進行了源代碼的代碼閱讀分析巨缘,關于Java8中的新增加的Stream API
特性在數(shù)據(jù)結構中的時候會在之后之后專門使用一篇文章的內(nèi)容進行介紹寞蚌,這里只介紹源代碼和代碼邏輯分析读存。
ArrayList關鍵詞
閱讀ArrayList的相關文檔润绎,很容易從中提取出如下的關鍵詞:
backing array
-
capacity
/incremental reallocation
-
structural modification
/fail-fast
綜合這三個關鍵詞峻凫,我們不難了解到ArrayList
的特性和問題炊汹。backing array
指出實現(xiàn)了ArrayList
的幕后機制的是一個幕后數(shù)組(backing array篡撵,Object數(shù)組)褐着,其中它的容量(capacity
)是可以增量調(diào)整的(incremental reallocation
)坷澡,并且ArrayList
并不像它的前輩Vector
是一種線程安全的容器,如果出現(xiàn)了結構性變化(structural modification含蓉,比如 add remove频敛,set不是 ,會通過modCount標記這個結構性變化)會使用一種機制馅扣,這種機制不會讓存在這缺陷的過程繼續(xù)下去斟赚,而是立刻停止系統(tǒng)工作,這種機制也被稱為fail-fast
差油。fail-fast
是一種盡最大努力(best-effort
)的機制拗军,不可以基于它拋出的異常做錯誤控制任洞。
從三個關鍵詞中衍生出三個問題
- 如果是一個Object數(shù)組,
ArrayList
如何實現(xiàn)類型檢查和相關的問題发侵? -
fail-fast
機制如何實現(xiàn)的交掏,具體如何體現(xiàn)? -
ArrayList
如何實現(xiàn)擴容的刃鳄?
關于這三個問題盅弛,希望讀者能在我的分析中思考,最后我也會給出答案叔锐。
源碼分析
ArrayList的繼承關系圖
其中
ArrayListSpliterator
和Strem API有關挪鹏,這里后續(xù)會詳細分析。
ArrayList中的關鍵常量
這些常量都是見名知意的愉烙,{}表示空數(shù)組字面量
名稱 | 類型 | 初始值 | 意義 |
---|---|---|---|
DEFAULT_CAPACITY | Object[] | 10 | 表示下面的DEFAULT空數(shù)組的大小 |
DEFAULTCAPACITY_EMPTY_ELEMENTDATA | Object[] | {} | |
elementData | Object[] | {} | |
EMPTY_ELEMENTDATA | Object[] | {} | 空數(shù)組大小為0讨盒,和DEFAULT數(shù)組相區(qū)別 |
size | int | 0 | List大小 |
modCount | int | 0 | fail-fast標記 |
MAX_ARRAY_SIZE | int | Integer.MAX_VALUE-8 | 最大大小 |
構造函數(shù)
ArrayList的中的三個構造函數(shù)
- 空構造函數(shù)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
直接初始化了elementData
為DEFAULT
數(shù)組,大小默認為10
- 帶有初始大小參數(shù)的構造函數(shù)
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);
}
}
- 將現(xiàn)有的Collection的內(nèi)容初始化ArrayList
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
中的元素的在ArrayList中的順序和Collection c
的iterator
的遍歷順序相同齿梁,具體的實現(xiàn)在toArray
函數(shù)中催植,這里采取ArrayList
和LinkedList
的toArray
作為參考:
- ArrayList
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
- LinkedList
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
ArrayList
中迭代器迭代的順序是按照數(shù)組的順序,LinkedList
中的順序是按照Node
連接的順序勺择,沒毛病创南。
這里看一下 打****的問題,這是一個官方bug省核,為什么toArray()
不一定能返回Object[]
? 事實上這是一個類型轉(zhuǎn)換的問題稿辙,這篇文章說的非常清晰,輕容許我簡要說明一下他的觀點:
public class MyList<E> extends ArrayList<E> {
// toArray() 的同名方法
public String[] toArray() {
return new String[]{"1", "2", "3"};
}
}
因為方法的重載不看返回值的气忠,如果子類定義了這個方法邻储,當調(diào)用toArray()
的時候,就回返回String[]
旧噪,這樣就會出現(xiàn)錯誤吨娜。
add相關操作
add
有很多相關方法,不一一列舉代碼了淘钟,具體實現(xiàn)大同小異宦赠。add
操作的代碼調(diào)用情況圖如下:
這些方法都依賴
ensureCapacity
相關方法,這里要著重分析一下米母。
ensure相關方法
ensure
相關方法都和擴容操作有關勾扭,minCapacity
就是如果elementData
容量不夠大,就會最小擴容到這個大小铁瞒,并且留意modCount
的變化
private void ensureCapacityInternal(int minCapacity) {
//封裝方法 詳細調(diào)用妙色,直接看后面
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果backing array是Default數(shù)組
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//體現(xiàn)了Default數(shù)組和其Default大小的對應關系
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return 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;
//首先嘗試擴容到原來的大小的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//下面的的兩個if語句指向了擴容情況的兩個極端:
//不夠minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//但是卻又大于最大數(shù)組大小
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
所以我們再看看最基本的add(E e)
方法干了什么,看一個方法锥惋,其余的同名方法的做法大同小異:
public boolean add(E e) {
//minCapacity = size + 1 然后在進行是否擴容的試探
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
綜上所述,add
操作都是結構性變化的操作饲握,通過ensure
相關函數(shù)進行擴容和modCount
的自增(fail-fast
防止多線程操作)栅表。再擴容期間笋鄙,每次都會擴容1.5倍,所以在感覺可能數(shù)據(jù)很多的情況下怪瓶,使用默認的無參數(shù)構造函數(shù)的所產(chǎn)生的10個空間是不夠的萧落。
remove相關操作
remove
操作也是一個結構性變化的操作,我們主要看看幾個修改modCount
的操作:fastRemove
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;
}
//fastRemove 之所以fast是因為不需要在這個方法中做邊界判斷洗贰,邊界判斷在上面的for循環(huán)已經(jīng)完成了
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; // clear to let GC do its work
}
batchRemove()
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
//使用了一種類似于雙指針的操作
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
//如果需要保留c中的元素找岖,complement取true
if (c.contains(elementData[r]) == complement)
//利用雙指針進行復制覆蓋原來的位置
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
//因為ArrayList是可以容納null元素的,所以contains不會拋出異常敛滋,但是有些容器不能容納null的時候许布,會從上面的if語句進入finally塊
//直接默認r后面的是需要保留的
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
//set不是結構性變化操作,刪除才是绎晃,所以是size - w
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
介紹完了modCount
的兩個主要修改的地方蜜唾,下面請看modCount
對于fail-fast的作用的體現(xiàn)。
checkForComodification()
ArrayList
有兩個迭代器Itr
和ListItr
庶艾,ListItr
在上面的圖中可以看出是繼承了Itr
的袁余。這篇優(yōu)秀的文章
中給我們展示了CME(ConcurrentModificationException
)的兩種出現(xiàn)的的情況,不難看出咱揍,CME常常出現(xiàn)在使用迭代器迭代的情況下颖榜,或者Java的語法糖foreach中,對List進行結構性修改煤裙。通過閱讀Itr
的源碼可以對CME的出現(xiàn)原因了解的非常清楚掩完。
Itr
中的重要常量:
名稱 | 類型 | 初始值 | 意義 |
---|---|---|---|
cursor | int | 0 | 游標 |
lastRet | int | -1 | |
expectedModCount | int | modCount | 在初始化Itr時會復制modCount,用于確保不會有其他線程對其進行修改 |
在Itr
的next
和remove
方法中都需要調(diào)用checkForComodification
方法:
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
在上面的文章中列舉的單線程情況中硼砰,因為ArrayList
的remove
方法雖然修改modCount
但是且蓬,和Itr
中的expectedModCount
不符,而導致異常题翰。說穿了恶阴,這實際上是因為外部類的modCount
和內(nèi)部類的expectedModCount
不相同導致的問題,這只能說是一個缺陷遍愿。在多線程的環(huán)境下存淫,使用modCount
才是比較好的控制策略耘斩。
SubList
SubList
是ArrayList
的一個內(nèi)部類沼填,其中的方法大體上和ArrayList
一致,可以看做是對ArrayList
的一個“視圖”括授,但是這個視圖是可以影響其原本映射的List
的坞笙。
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
問題解答和總結
雖然這篇文章并沒有對于整體的所有代碼進行逐行解釋岩饼,但是大體上能夠讓讀者對于ArrayList
源碼有一個直觀地認識。
下面來回答文章開頭提的幾個問題:
-
fail-fast
是通過modCount
和Itr
還有ListItr
薛夜,SubList
等中的checkForComodification
操作實現(xiàn)的籍茧。但是在foreach和while中使用迭代器模式進行遍歷時,禁止使用ArrayList
的remove
和add
操作梯澜。這樣的機制在最大程度上避免了多線程修改寞冯。 - 擴容機制是通過
ensure
函數(shù)實現(xiàn)的,如果大小不夠會通過擴充1.5倍看看晚伙。 - 類型檢查吮龄。首先使用了靜態(tài)類型檢查,泛型做了一定的限定咆疗。其次漓帚,針對
toArray()
這里天然的語法缺陷,也做了個邏輯層面的判斷午磁,規(guī)避類型不一產(chǎn)生的問題尝抖。