遍歷HashMap的方法有多種,比如通過獲取map的keySet, entrySet, iterator之后,都可以實現遍歷,然而如果在遍歷過程中對map進行讀取之外的操作則需要注意使用的遍歷方式和操作方法。
public class MapIteratorTest {
private static Map map = new HashMap();
public static void main(String[] args) {
//init
for(int i = 0; i < 10; i++){
map.put(i, "value" + i);
}
for(Map.Entry entry : map.entrySet()){
Integer key = entry.getKey();
if(key % 2 == 0){
System.out.println("To delete key " + key);
map.remove(key);
System.out.println("The key " + + key + " was deleted");
}
}
System.out.println("map size = " + map.size());
for(Map.Entry entry : map.entrySet()){
System.out.println( entry.getKey() +" = " + entry.getValue());
}
}
}
上面代碼的輸出結果為
To delete key 0
The key 0 was deleted
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
at java.util.HashMap$EntryIterator.next(HashMap.java:834)
at java.util.HashMap$EntryIterator.next(HashMap.java:832)
at com.gpzuestc.collection.MapIteratorTest.main(MapIteratorTest.java:60)
通過上面的輸出可以發現第一個偶數key元素已經被成功remove,異常的拋出位置是在迭代器遍歷下一個元素的時候。
如果把上面高亮的遍歷代碼替換成keySet的方式,通過keySet的remove操作同樣會在遍歷下個元素時拋出異常,示例如下。
12345678
SetkeySet=map.keySet();for(Integerkey:keySet){if(key%2==0){System.out.println("To delete key "+key);keySet.remove(key);System.out.println("The key "++key+" was deleted");}}
123456
Todeletekey0Thekey0wasdeletedExceptioninthread"main"java.util.ConcurrentModificationExceptionatjava.util.HashMap$HashIterator.nextEntry(HashMap.java:793)atjava.util.HashMap$KeyIterator.next(HashMap.java:828)atcom.gpzuestc.collection.MapIteratorTest.main(MapIteratorTest.java:49)
如果要實現遍歷過程中進行remove操作,上面兩種方式都不能使用,而是需要通過顯示獲取keySet或entrySet的iterator來實現。
1234567891011
Iterator>it=map.entrySet().iterator();while(it.hasNext()){Map.Entryentry=it.next();Integerkey=entry.getKey();if(key%2==0){System.out.println("To delete key "+key);it.remove();System.out.println("The key "++key+" was deleted");}}
12345678910111213141516
Todeletekey0Thekey0wasdeletedTodeletekey2Thekey2wasdeletedTodeletekey4Thekey4wasdeletedTodeletekey6Thekey6wasdeletedTodeletekey8Thekey8wasdeletedmapsize=51=value13=value35=value57=value79=value9
分析原因
其實上面的三種遍歷方式從根本上講都是使用的迭代器,之所以出現不同的結果是由於remove操作的實現不同決定的。
首先前兩種方法都在調用nextEntry方法的同一個地方拋出了異常
1234567
finalEntrynextEntry(){if(modCount!=expectedModCount)thrownewConcurrentModificationException();Entrye=next;......}
這里modCount是表示map中的元素被修改了幾次(在移除,新加元素時此值都會自增),而expectedModCount是表示期望的修改次數,在迭代器構造的時候這兩個值是相等,如果在遍歷過程中這兩個值出現了不同步就會拋出ConcurrentModificationException異常。
1、HashMap的remove方法實現
1234
publicVremove(Objectkey){Entrye=removeEntryForKey(key);return(e==null?null:e.value);}
2、HashMap.KeySet的remove方法實現
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
3、HashMap.HashIterator的remove方法實現
12345678910
publicvoidremove(){if(current==null)thrownewIllegalStateException();if(modCount!=expectedModCount)thrownewConcurrentModificationException();Objectk=current.key;current=null;HashMap.this.removeEntryForKey(k);expectedModCount=modCount;}
以上三種實現方式都通過調用HashMap.removeEntryForKey方法來實現刪除key的操作。在removeEntryForKey方法內只要移除了key modCount就會執行一次自增操作,此時modCount就與expectedModCount不一致了,上面三種remove實現中,只有第三種iterator的remove方法在調用完removeEntryForKey方法后同步了expectedModCount值與modCount相同,所以在遍歷下個元素調用nextEntry方法時,iterator方式不會拋異常。
1234567891011121314151617181920212223242526
finalEntryremoveEntryForKey(Objectkey){inthash=(key==null)?0:hash(key.hashCode());inti=indexFor(hash,table.length);Entryprev=table[i];Entrye=prev;while(e!=null){Entrynext=e.next;Objectk;if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k)))){modCount++;size--;if(prev==e)table[i]=next;elseprev.next=next;e.recordRemoval(this);returne;}prev=e;e=next;}returne;}
發散
1、如果是遍歷過程中增加或修改數據呢?
增加或修改數據只能通過Map的put方法實現,在遍歷過程中修改數據可以,但如果增加新key就會在下次循環時拋異常,因為在添加新key時modCount也會自增。
2、有些集合類也有同樣的遍歷問題,如ArrayList,通過Iterator方式可正確遍歷完成remove操作,直接調用list的remove方法就會拋異常。
//會拋ConcurrentModificationException異常for(String str : list){
list.remove(str);
}
//正確遍歷移除方式
Iterator it = list.iterator();
while(it.hasNext()){
it.next();
it.remove();
}
3、jdk為什么這樣設計,只允許通過iterator進行remove操作?
HashMap和keySet的remove方法都可以通過傳遞key參數刪除任意的元素,而iterator只能刪除當前元素(current),一旦刪除的元素是iterator對象中next所正在引用的,如果沒有通過modCount、 expectedModCount的比較實現快速失敗拋出異常,下次循環該元素將成為current指向,此時iterator就遍歷了一個已移除的過期數據。