今天修改一個bug,需要取一個List和一個Set的交集,使用了雙重循環。想着提高循環效率,每加入一個交集中的元素,就將List中的元素刪除,減少不必要的循環。結果直接調用了List的remove()方法,拋出了java.util.ConcurrentModificationException異常。這時才忽然記起之前看過的List循環中使用remove()方法要特別注意,尤其是forEach的循環。
不使用forEach的循環
使用常規的for循環寫法,代碼如下:
public static void main(String [] args){
List<String> list = new ArrayList<String>();
list.add("111");
list.add("111");
list.add("111");
list.add("333");
list.add("333");
for (int i = 0; i < list.size(); i++){
if ("111".equals(list.get(i))){
list.remove("111");
// i--;不加這句會少刪除一個111
}
System.out.println(list.size());
}
}
先說說list的remove()方法,該方法有兩個,一個是remove(Object obj),另一個是remove(int index)。根據參數很容易理解,而這里要說的是remove(obj)會刪除list中的第一個該元素,remove(index)會刪除該下標的元素。調用一次remove方法,會使list中的所有該刪除元素后面的元素前移。看看一個remove(Object obj)的源碼:
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;
}
remove是可以刪除null的,但一般情況都是走else路徑,再看看faseRemove方法:
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; // Let gc do its work
}大專欄 List.remove()的使用注意>
System.arraycopy就是將list中刪除元素的后面遷移,暫時沒繼續深入了解。總得來說,這樣的操作是因為沒有對變化的list做出相應的下標處理而產生了錯誤的結果,但是並沒有產生異常。
使用forEach循環
錯誤的使用代碼如下:
public static void main(String [] args){
List<String> list = new ArrayList<String>();
list.add("111");
list.add("111");
list.add("111");
list.add("333");
list.add("333");
for (String str : list){
if("111".equals(str)){
list.remove(str);//拋出異常java.util.ConcurrentModificationException
}
}
}
這樣使用會拋出java.util.ConcurrentModificationException。在forEach中,遍歷的集合都必須實現Iterable接口(數組除外)。而forEach的寫法實際是對Iterator遍歷的簡寫,類似於以下代碼:
public void display(){
for(String s : strings){
System.out.println(s);
}
Iterator<String> iterator = strings.iterator();
while(iterator.hasNext()){
String s = iterator.next();
System.out.println(s);
}
}
上面的forEach和下面的Iterator迭代器效果一樣,涉及到編譯原理的一些內容,就不深究了。可以理解為forEach將其中的集合轉為了迭代器進行遍歷,而該迭代器內部有一個next()方法,代碼如下:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
可以看到在使用next()方法獲取下一個元素的之前會先檢查迭代器修改次數,在我們使用ArrayList的remove()方法刪除元素時,實際只修改了modCount,這樣就會造成modCount和expectedModCount不相等,從而拋出異常。而使用迭代器本身的remove()方法則不會,因為Iterator本身的remove()方法會同時修改modCount和expectedModCount。
引用一段網上的解釋:
Iterator是工作在一個獨立的線程中,並且擁有一個mutex鎖。 Iterator被創建之后會建立一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變,所以當索引指針往后移動的時候就找不到要迭代的對象,所以按照fail-fast原則Iterator會馬上拋出java.util.ConcurrentModificationException異常。所以Iterator在工作的時候是不允許被迭代的對象被改變的。但你可以使用Iterator本身的方法remove()來刪除對象,Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。
最后總結一下就是,forEach將List轉為了Iterator,刪除元素就需要使用Iterator的remove()方法,錯誤地使用了List.remove()方法就會拋出異常。
