在 Java 開發(fā)手冊中,有這樣一條規(guī)定:

但是手冊中并沒有給出具體原因,本文就來深入分析一下該規(guī)定背后的思考。
foreach 循環(huán)
Foreach 循環(huán)(Foreach loop)是計算機編程語言中的一種控制流程語句,通常
用來循環(huán)遍歷數(shù)組或集合中的元素。
Java 語 言 從 JDK 1.5.0 開 始 引 入 foreach 循 環(huán)。 在 遍 歷 數(shù) 組、 集 合 方 面,
為什么禁止在 foreach 循環(huán)里進行元素的 remove/add 操作? < 61
foreach 為開發(fā)人員提供了極大的方便。
foreach 語法格式如下:
for( 元素類型 t 元素變量 x : 遍歷對象 obj){
引用了 x 的 java 語句 ;
}
以下實例演示了 普通 for 循環(huán) 和 foreach 循環(huán)使用:
public static void main(String[] args) {
// 使用 ImmutableList 初始化一個 List
List<String> userNames = ImmutableList.of("Hollis", "hollis",
"HollisChuang", "H");
System.out.println(" 使用 for 循環(huán)遍歷 List");
for (int i = 0; i < userNames.size(); i++) {
System.out.println(userNames.get(i));
}
System.out.println(" 使用 foreach 遍歷 List");
for (String userName : userNames) {
System.out.println(userName);
}
}
以上代碼運行輸出結(jié)果為:
使用 for 循環(huán)遍歷 List
Hollis
hollis
HollisChuang
H
使用 foreach 遍歷 List
Hollis
hollis
HollisChuang
H
可以看到,使用 foreach 語法遍歷集合或者數(shù)組的時候,可以起到和普通 for
循環(huán)同樣的效果,并且代碼更加簡潔。所以,foreach 循環(huán)也通常也被稱為增強 for
循環(huán)。
但是,作為一個合格的程序員,我們不僅要知道什么是增強 for 循環(huán),還需要知
道增強 for 循環(huán)的原理是什么?
其實,增強 for 循環(huán)也是 Java 給我們提供的一個語法糖,如果將以上代碼編譯
后的 class 文件進行反編譯(使用 jad 工具)的話,可以得到以下代碼
Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("Hollis"))
userNames.remove(userName);
} while(true);
System.out.println(userNames);
可以發(fā)現(xiàn),原本的增強 for 循環(huán),其實是依賴了 while 循環(huán)和 Iterator 實現(xiàn)的。
(請記住這種實現(xiàn)方式,后面會用到!)
問題重現(xiàn)
規(guī)范中指出不讓我們在 foreach 循環(huán)中對集合元素做 add/remove 操作,那么,
我們嘗試著做一下看看會發(fā)生什么問題。
// 使用雙括弧語法(double-brace syntax)建立并初始化一個 List
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
for (int i = 0; i < userNames.size(); i++) {
if (userNames.get(i).equals("Hollis")) {
userNames.remove(i);
}
}
System.out.println(userNames);
以上代碼,首先使用雙括弧語法(double-brace syntax)建立并初始化一個
List,其中包含四個字符串,分別是 Hollis、hollis、HollisChuang 和 H。
然后使用普通 for 循環(huán)對 List 進行遍歷,刪除 List 中元素內(nèi)容等于 Hollis 的元
素。然后輸出 List,輸出結(jié)果如下:
[hollis, HollisChuang, H]
以上是哪使用普通的 for 循環(huán)在遍歷的同時進行刪除,那么,我們再看下,如果
使用增強 for 循環(huán)的話會發(fā)生什么:
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove(userName);
}
}
System.out.println(userNames);
以上代碼,使用增強 for 循環(huán)遍歷元素,并嘗試刪除其中的 Hollis 字符串元素。
運行以上代碼,會拋出以下異常:
java.util.ConcurrentModificationException
同樣的,讀者可以嘗試下在增強 for 循環(huán)中使用 add 方法添加元素,結(jié)果也會同
樣拋出該異常。
之所以會出現(xiàn)這個異常,是因為觸發(fā)了一個 Java 集合的錯誤檢測機制——failfast 。
fail-fast
接下來,我們就來分析下在增強 for 循環(huán)中 add/remove 元素的時候會拋出
java.util.ConcurrentModificationException 的原因,即解釋下到底什么是 fail-fast
進制,fail-fast 的原理等。
fail-fast,即快速失敗,它是 Java 集合的一種錯誤檢測機制。當(dāng)多個線程對集
合(非 fail-safe 的集合類)進行結(jié)構(gòu)上的改變的操作時,有可能會產(chǎn)生 fail-fast 機
制,這個時候就會拋出 ConcurrentModificationException(當(dāng)方法檢測到對象的并
發(fā)修改,但不允許這種修改時就拋出該異常)。
同時需要注意的是,即使不是多線程環(huán)境,如果單線程違反了規(guī)則,同樣也有可
能會拋出改異常。
那么,在增強 for 循環(huán)進行元素刪除,是如何違反了規(guī)則的呢?
要分析這個問題,我們先將增強 for 循環(huán)這個語法糖進行解糖,得到以下代碼:
public static void main(String[] args) {
// 使用 ImmutableList 初始化一個 List
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("Hollis"))
userNames.remove(userName);
} while(true);
System.out.println(userNames);
}
然后運行以上代碼,同樣會拋出異常。我們來看一下 ConcurrentModificationException 的完整堆棧:

通過異常堆棧我們可以到,異常發(fā)生的調(diào)用鏈 ForEachDemo 的第 23 行,
Iterator.next 調(diào)用了 Iterator.checkForComodification 方法 ,而異常就
是 checkForComodification 方法中拋出的。
其 實, 經(jīng) 過 debug 后, 我 們 可 以 發(fā) 現(xiàn), 如 果 remove 代 碼 沒 有 被 執(zhí) 行 過,
iterator.next 這一行是一直沒報錯的。拋異常的時機也正是 remove 執(zhí)行之后的的那
一次 next 方法的調(diào)用。
我們直接看下 checkForComodification 方法的代碼,看下拋出異常的原因:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
代 碼 比 較 簡 單,modCount != expectedModCount 的 時 候, 就 會 拋 出
ConcurrentModificationException。
那么,就來看一下,remove/add 操作室如何導(dǎo)致 modCount 和 expectedModCount 不相等的吧。
remove/add 做了什么
首先,我們要搞清楚的是,到底 modCount 和 expectedModCount 這兩個變
量都是個什么東西。
通過翻源碼,我們可以發(fā)現(xiàn):
● modCount 是 ArrayList 中的一個成員變量。它表示該集合實際被修改的次
數(shù)。
● expectedModCount 是 ArrayList 中的一個內(nèi)部類——Itr 中的成員變量。
expectedModCount 表示這個迭代器期望該集合被修改的次數(shù)。其值是在
ArrayList.iterator 方法被調(diào)用的時候初始化的。只有通過迭代器對集合進行操
作,該值才會改變。
● Itr 是一個 Iterator 的實現(xiàn),使用 ArrayList.iterator 方法可以獲取到的迭代器
就是 Itr 類的實例。
他們之間的關(guān)系如下:
class ArrayList{
private int modCount;
public void add();
public void remove();
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
}
public Iterator<E> iterator() {
return new Itr();
}
}
其實,看到這里,大概很多人都能猜到為什么 remove/add 操作之后,會導(dǎo)致
expectedModCount 和 modCount 不想等了。
通過翻閱代碼,我們也可以發(fā)現(xiàn),remove 方法核心邏輯如下:

可以看到,它只修改了 modCount,并沒有對 expectedModCount 做任何
操作。
簡單總結(jié)一下,之所以會拋出 ConcurrentModificationException 異常,是因
為我們的代碼中使用了增強 for 循環(huán),而在增強 for 循環(huán)中,集合遍歷是通過 iterator
進行的,但是元素的 add/remove 卻是直接使用的集合類自己的方法。這就導(dǎo)致
iterator 在遍歷的時候,會發(fā)現(xiàn)有一個元素在自己不知不覺的情況下就被刪除 / 添加
了,就會拋出一個異常,用來提示用戶,可能發(fā)生了并發(fā)修改!
正確姿勢
至此,我們介紹清楚了不能在 foreach 循環(huán)體中直接對集合進行 add/remove
操作的原因。
但是,很多時候,我們是有需求需要過濾集合的,比如刪除其中一部分元素,那
么應(yīng)該如何做呢?有幾種方法可供參考:
1. 直接使用普通 for 循環(huán)進行操作
我們說不能在 foreach 中進行,但是使用普通的 for 循環(huán)還是可以的,因為普通
for 循環(huán)并沒有用到 Iterator 的遍歷,所以壓根就沒有進行 fail-fast 的檢驗。
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
for (int i = 0; i < 1; i++) {
if (userNames.get(i).equals("Hollis")) {
userNames.remove(i);
}
}
System.out.println(userNames);
這種方案其實存在一個問題,那就是 remove 操作會改變 List 中元素的下標,
可能存在漏刪的情況。
2. 直接使用 Iterator 進行操作
除了直接使用普通 for 循環(huán)以外,我們還可以直接使用 Iterator 提供的 remove
方法。
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
Iterator iterator = userNames.iterator();
while (iterator.hasNext()) {
if (iterator.next().equals("Hollis")) {
iterator.remove();
}
}
System.out.println(userNames);
如果直接使用 Iterator 提供的 remove 方法,那么就可以修改到 expectedModCount 的值。那么就不會再拋出異常了。其實現(xiàn)代碼如下:

3. 使用 Java 8 中提供的 filter 過濾
Java 8 中可以把集合轉(zhuǎn)換成流,對于流有一種 filter 操作, 可以對原始 Stream
進行某項測試,通過測試的元素被留下來生成一個新 Stream。
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
userNames = userNames.stream().filter(userName -> !userName.
equals("Hollis")).collect(Collectors.toList());
System.out.println(userNames);
4. 使用增強 for 循環(huán)其實也可以
如果,我們非常確定在一個集合中,某個即將刪除的元素只包含一個的話, 比如
對 Set 進行操作,那么其實也是可以使用增強 for 循環(huán)的,只要在刪除之后,立刻結(jié)
束循環(huán)體,不要再繼續(xù)進行遍歷就可以了,也就是說不讓代碼執(zhí)行到下一次的 next
方法。
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove(userName);
break;
}
}
System.out.println(userNames);
5. 直接使用 fail-safe 的集合類
在 Java 中,除了一些普通的集合類以外,還有一些采用了 fail-safe 機制的集
合類。這樣的集合容器在遍歷時不是直接在集合內(nèi)容上訪問的,而是先復(fù)制原有集合
內(nèi)容,在拷貝的集合上進行遍歷。
由于迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改
并不能被迭代器檢測到,所以不會觸發(fā) ConcurrentModificationException。
ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove();
}
}
基于拷貝內(nèi)容的優(yōu)點是避免了 ConcurrentModificationException,但同樣地,
迭代器并不能訪問到修改后的內(nèi)容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合
拷貝,在遍歷期間原集合發(fā)生的修改迭代器是不知道的。
為什么禁止在 foreach 循環(huán)里進行元素的 remove/add 操作? < 71
java.util.concurrent 包下的容器都是安全失敗,可以在多線程下并發(fā)使用,并
發(fā)修改。
總結(jié)
我們使用的增強 for 循環(huán),其實是 Java 提供的語法糖,其實現(xiàn)原理是借助
Iterator 進行元素的遍歷。
但是如果在遍歷過程中,不通過 Iterator,而是通過集合類自身的方法對集合進
行添加 / 刪除操作。那么在 Iterator 進行下一次的遍歷時,經(jīng)檢測發(fā)現(xiàn)有一次集合的
修改操作并未通過自身進行,那么可能是發(fā)生了并發(fā)被其他線程執(zhí)行的,這時候就會
拋出異常,來提示用戶可能發(fā)生了并發(fā)修改,這就是所謂的 fail-fast 機制。
當(dāng)然還是有很多種方法可以解決這類問題的。比如使用普通 for 循環(huán)、使用
Iterator 進行元素刪除、使用 Stream 的 filter、使用 fail-safe 的類等。