《阿里巴巴JAVA開發(fā)手冊》中有這樣一條:
不要在 foreach 循環(huán)里進(jìn)行元素的 add / remove 操作盲泛,remove 元素使用 Iterator 方式疗隶。
經(jīng)測試霞扬,當(dāng)在 foreach 循環(huán)中 add / remove 集合元素糕韧,可能會(huì)拋出 ConcurrentModificationException 異常枫振,下面介紹進(jìn)行詳細(xì)說明。
1. foreach循環(huán)
foreach 又稱為增強(qiáng)型for循環(huán)萤彩,通過打印以下代碼的class字節(jié)碼粪滤,我們來了解下其內(nèi)部實(shí)現(xiàn)。
List<String> list = new ArrayList<> ();
list.add("a");
list.add("b");
for (String item : list) {
System.out.println(item);
}
使用javap -c 命令查看class文件的字節(jié)碼:
由上圖紅框圈起的部分不難發(fā)現(xiàn)雀扶,foreach 循環(huán)內(nèi)部實(shí)際是通過 Iterator 實(shí)現(xiàn)的杖小,以上代碼等同于:
List<String> list = new ArrayList<> ();
list.add("a");
list.add("b");
for (Iterator<String> i = list.iterator(); i.hasNext(); ) {
String item = i.next();
System.out.println(item);
}
實(shí)際上,foreach 循環(huán)僅是 Java 提供的語法糖愚墓。編譯器隱藏了對(duì) Iterator 的使用予权,使得 foreach 在語法上較傳統(tǒng) for 循環(huán)更加簡潔,也不容易出錯(cuò)浪册。下面我們看下 Iterator.
2. Iterator
Iterator 接口包含以下幾個(gè)主要方法:
boolean hasNext(); // 檢查是否有下個(gè)元素
E next(); // 獲取下個(gè)元素
void remove(); // 移除當(dāng)前指向的元素
在Java集合框架中扫腺,各集合內(nèi)部都實(shí)現(xiàn)了 Iterator 接口,用以對(duì)集合元素進(jìn)行遍歷村象、修改笆环。我們看下 ArrayList 內(nèi)部的 Iterator 實(shí)現(xiàn)。
3. ArrayList$Itr
ArrayList 的內(nèi)部類 Itr 實(shí)現(xiàn)了 Iterator 接口煞肾,Itr 共有3個(gè)成員變量:
private class Itr implements Iterator<E> {
int cursor; // 下一次遍歷的元素的位置
int lastRet = -1; // 前一次返回的元素的位置
int expectedModCount = modCount;
值得注意的是 expectedModCount 這個(gè)變量咧织,其初始值為 modCount.
3.1 modCount
modCount 是 ArrayList 繼承自 AbstractList 的一個(gè)變量。在AbstractList的源碼注釋中籍救,是這樣解釋這個(gè)變量的:
The number of times this list has been structurally modified. Structural modifications are those that change the size of the list.
翻譯成中文大意為:modCount 為 list 的結(jié)構(gòu)變化次數(shù)习绢,即 list 的元素?cái)?shù)量變化次數(shù)。
查看 ArrayList 的源碼蝙昙,會(huì)發(fā)現(xiàn)在每次調(diào)用 add() 和 remove() 方法闪萄,都會(huì)進(jìn)行 modCount++ 操作。
3.2 expectedModCount
modCount 意為 list 的結(jié)構(gòu)變化次數(shù)奇颠,而 expectedModCount 可被視為 Itr 內(nèi)部記錄的集合結(jié)構(gòu)變化次數(shù)败去,那么該變量有何作用呢?
在 Itr 內(nèi)部有一個(gè) checkForComodification 方法烈拒,如下所示:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
當(dāng)集合的實(shí)際結(jié)構(gòu)變化次數(shù) 和 Itr 記錄的變化次數(shù)不相等時(shí)圆裕,則拋出文章開頭提到的 ConcurrentModificationException 異常。而在 Itr 的 next() 方法 和 remove() 中都調(diào)用了 checkForComodification 方法荆几。
4. 結(jié)論
由上文吓妆,我們可以得出為何不能在 foreach 循環(huán)中 add/remove 元素的結(jié)論。
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if (item.equals("2")) {
list.remove(item);
}
}
相當(dāng)于:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
String item = iterator.next();
if (item.equals("2")) {
list.remove(item);
}
}
- 在進(jìn)行了 2 次 add 元素后吨铸,ArrayList 內(nèi)部的 modCount = 2行拢;
- 在進(jìn)行 foreach 循環(huán)時(shí),隱式調(diào)用的 Itr 內(nèi)部 expectedModCount 同樣初始化為 2诞吱;
- 在 foreach 循環(huán)中 remove 元素時(shí)舟奠,由于調(diào)用的是 list 的 remove 方法竭缝,會(huì)使 modCount + 1 = 3.
- 調(diào)用 Iterator.next() 方法獲取下個(gè)元素前,iterator 檢查到 modCount != expectedModCount沼瘫,拋出 ConcurrentModificationException 異常抬纸。
4.1 為什么使用 Iterator 的 remove 方法操作集合元素不會(huì)有問題?
同樣我們看下 ArrayList 內(nèi)部 Itr 的 remove 方法的源碼:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet); // 調(diào)用集合的remove()方法
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; // 更新expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
實(shí)際上晕鹊,調(diào)用 Itr 的 remove() 方法移除集合元素時(shí)松却,首先會(huì)調(diào)用 ArrayList 的 remove() 方法,再對(duì) expectedModCount 進(jìn)行更新溅话。在下次調(diào)用 Itr.next() 方法獲取下個(gè)元素時(shí)晓锻,不會(huì)出現(xiàn) expectedModCount != modCount 的情況。
4.2 Iterator 為什么要檢查集合的結(jié)構(gòu)變化次數(shù)?
這其實(shí)是為了防止多線程并發(fā)修改集合飞几,在一個(gè)線程遍歷集合的同時(shí)砚哆,另一個(gè)線程同時(shí)增刪集合元素,將無法保證數(shù)據(jù)的一致性屑墨,集合的遍歷過程也將被打亂躁锁。采用 modCount 機(jī)制,在此情景下及時(shí)拋出異常卵史,確保同一時(shí)間只會(huì)有一個(gè)線程修改或遍歷集合战转,也即 fail-fast 策略。