在 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 集合的一種錯誤檢測機制。當多個線程對集
合(非 fail-safe 的集合類)進行結(jié)構(gòu)上的改變的操作時翠勉,有可能會產(chǎn)生 fail-fast 機
制妖啥,這個時候就會拋出 ConcurrentModificationException(當方法檢測到對象的并
發(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 操作室如何導致 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 操作之后,會導致
expectedModCount 和 modCount 不想等了粱锐。
通過翻閱代碼疙挺,我們也可以發(fā)現(xiàn),remove 方法核心邏輯如下:
可以看到怜浅,它只修改了 modCount铐然,并沒有對 expectedModCount 做任何
操作。
簡單總結(jié)一下恶座,之所以會拋出 ConcurrentModificationException 異常搀暑,是因
為我們的代碼中使用了增強 for 循環(huán),而在增強 for 循環(huán)中跨琳,集合遍歷是通過 iterator
進行的自点,但是元素的 add/remove 卻是直接使用的集合類自己的方法。這就導致
iterator 在遍歷的時候脉让,會發(fā)現(xiàn)有一個元素在自己不知不覺的情況下就被刪除 / 添加
了桂敛,就會拋出一個異常,用來提示用戶溅潜,可能發(fā)生了并發(fā)修改术唬!
正確姿勢
至此,我們介紹清楚了不能在 foreach 循環(huán)體中直接對集合進行 add/remove
操作的原因滚澜。
但是粗仓,很多時候,我們是有需求需要過濾集合的,比如刪除其中一部分元素借浊,那
么應該如何做呢塘淑?有幾種方法可供參考:
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)容上訪問的囚玫,而是先復制原有集合
內(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 機制兔港。
當然還是有很多種方法可以解決這類問題的。比如使用普通 for 循環(huán)仔拟、使用
Iterator 進行元素刪除押框、使用 Stream 的 filter、使用 fail-safe 的類等理逊。