Java基礎之你會在foreach遍歷集合時進行remove操作嗎?
當通過for迴圈遍歷集合時,一般禁止操作(add or remove)集合元素。雖然開發規範裡寫的非常清楚,但最近還是有人掉坑裡導致出了一個小BUG,那我們就一起看看這麼做到底會發生什麼?
小例子
程式碼示例
List<String> list = new ArrayList<>();
list.add("e1");
list.add("e2");
for (String str : list) {
if ("e1".equals(str)) {
list.remove("e1");
}
if ("e2".equals(str)) {
System.out.println("element 2 fetched" );
}
}
執行結果:element 2 fetched 將不會被列印。
位元組碼中是如何處理的?
讓我們看看位元組碼是怎麼樣的,僅截圖了部分位元組碼。
如上面截圖的 #27、#34、#43,foreach 實際上是通過 Iterator 來處理的。最後通過 #87 的 goto 指令進入下一次遍歷,並進行 hasNext()判斷。
class檔案反編譯後又是怎麼樣的?
再來看看將.class檔案反編譯後得到的程式碼,實際上編譯器將 foreach 轉換成了用 Iterator 來處理。
所以,眼見不一定為實,程式設計師開發時用的是高階語言,編碼怎麼簡單高效怎麼來,所以偶爾也可以看看反編譯class後的程式碼以及位元組碼檔案,看看編譯器做了哪些優化。
ArrayList list = new ArrayList();
list.add("e1");
list.add("e2");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String str = (String)var2.next();
if("e1".equals(str)) {
list.remove("e1");
}
if("e2".equals(str)) {
System.out.println("element 2 fetched" );
}
}
為什麼remove(e1)會導致無法獲取e2?
當 list.remove(“e1”)後,在 while(var2.hasNext()) 時,返回結果將為 false,因此當迴圈一次後Iterator將認為list已經遍歷結束。
要弄清原因,需要看看ArrayList對於Iterator介面的實現,瞭解hasNext()、next()方法的實現。
先看看ArrayList中實現Iterator的內部類Itr。
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
...
}
cursor表示下一個返回元素的下標,可以理解成 遊標;lastRet表示上一次返回的元素下標。另ArrayList有個size屬性,表示ArrayList中的元素個數。
hasNext() 的判斷條件是cursor != size. 只要沒遍歷到最後一個元素,就返回true.
public boolean hasNext() {
return cursor != size;
}
下面是 next() 部分程式碼。
public E next() {
...
int i = cursor; // cursor為當前需要返回元素的下標
...
cursor = i + 1; // cursor向後移動一個位置,指向下一個要返回的元素
return (E) elementData[lastRet = i]; // 對lastRet賦值,然後返回當前元素
}
現在,看一下下面程式碼的執行情況:
ArrayList list = new ArrayList();
list.add("e1");
list.add("e2");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String str = (String)var2.next();
if("e1".equals(str)) {
list.remove("e1");
}
}
- 第一次 呼叫var2.hasNext(),此時滿足條件 cursor(0) != size(2),然後執行 var2.next(),此時 cursor=1
- 執行 list.remove(“e1”),此時,list的size將從2變為1
- 當執行完第一次迴圈,進入第二次hasNext()判斷時,cursor=1而且size=1,導致Iterator認為已經遍歷結束,因此e2將被漏掉。
此時,過程已非常清楚。list本有2個元素,Iterator第一次獲取元素時,程式刪掉了當前元素,導致list的size變為1。Iterator第二次獲取元素時,開心說到:”list一共只有一個元素,我已經遍歷了一個,easy,輕鬆搞定!”。
矛盾點在於:hasNext() 是根據已fetch元素和被遍歷物件的size動態判斷的,一旦遍歷過程中被遍歷物件的size變化,就會出現以上問題。
用普通for迴圈進行處理
如果在普通for迴圈中進行如上操作,又會發生什麼呢?
List<String> list = new ArrayList<>();
list.add("e1");
list.add("e2");
for (int i = 0, length = list.size(); i < length; i++) {
if ("e1".equals(list.get(i))) {
list.remove("e1");
}
}
執行後將報如下異常:
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
原因:區域性變數length為list遍歷前的size,length=2;remove(“e1”)後,list的size變為1;因此,第二次進入迴圈執行list.get(1)時將出現上述異常
正確的姿勢
將remove操作交給Iterator來處理,使用Iterator介面提供的remove操作。
List<String> list = new ArrayList<>();
list.add("e1");
list.add("e2");
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
String str = iterator.next();
if ("e1".equals(str)) {
iterator.remove();
}
if ("e2".equals(str)) {
System.out.println("element 2 fetched");
}
}
執行結果:element 2 fetched 被正常打印出來。
那Iterator的remove()又是怎麼做的?下面是ArrayList中迭代器的remove方法。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet); // 呼叫ArrayList的remove移除元素,且size減1
cursor = lastRet; // 將遊標回退一位
lastRet = -1; // 重置lastRet
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
因為Iterator.remove()在執行集合本身的remove後,同時對遊標進行了 “校準”。
關於ConcurrentModificationException
以下Demo將丟擲該異常。
private static List<String> list = new ArrayList<>();
private static boolean isListUpdated = false;
public static void main(String[] args) throws InterruptedException {
list.add("e1");
list.add("e2");
new Thread(() -> {
list.add("e3");
isListUpdated = true;
}).start();
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
while (!isListUpdated) {
Thread.sleep(1000);
}
iterator.next();
}
}
在Java集合框架中,很多物件都不是執行緒安全的,例如:HashMap、ArrayList等。當Iterator在遍歷集合時,如果其他執行緒操作了集合中的元素,將丟擲該異常。
ArrayList中對於Iterator的實現類為Itr如下:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
}
其中有個重要的屬性 expectedModCount,表示本次期望修改的次數,初始值為modCount.
modCount 是 AbstractList 的屬性,如下:
protected transient int modCount = 0;
注意,它由transient修飾,保證了執行緒之間修改的可見性。對集合中物件的增加、刪除操作都會對modCount加1。
在next()、remove()操作中都會進行 checkForComodification() ,用於檢查迭代期間其他執行緒是否修改了被迭代物件。下面是checkForComodification方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
這是一種 Fail-Fast(快速失敗) 策略,只要被迭代物件發生變更,將滿足 modCount != expectedModCount 條件,從而丟擲ConcurrentModificationException。
歡迎關注陳同學的公眾號,一起學習,一起成長