Java遍歷HashMap並修改(remove)(轉載)
遍歷HashMap的方法有多種,比如通過獲取map的keySet, entrySet, iterator之後,都可以實現遍歷,然而如果在遍歷過程中對map進行讀取之外的操作則需要註意使用的遍歷方式和操作方法。
public class MapIteratorTest { private static Map<Integer, String> map = new HashMap<Integer, String>(); public static void main(String[] args) { //init for(int i = 0; i < 10; i++){ map.put(i, "value" + i); } for(Map.Entry<Integer, String> 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<Integer, String> 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操作同樣會在遍歷下個元素時拋出異常,示例如下。
Set<Integer> keySet = map.keySet(); for(Integer key : keySet){ if(key % 2 == 0){ System.out.println("To delete key " + key); keySet.remove(key); System.out.println("The key " + + key + " was deleted"); } }
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$KeyIterator.next(HashMap.java:828) at com.gpzuestc.collection.MapIteratorTest.main(MapIteratorTest.java:49)
如果要實現遍歷過程中進行remove操作,上面兩種方式都不能使用,而是需要通過顯示獲取keySet或entrySet的iterator來實現。
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator(); while(it.hasNext()){ Map.Entry<Integer, String> entry = it.next(); Integer key = entry.getKey(); if(key % 2 == 0){ System.out.println("To delete key " + key); it.remove(); System.out.println("The key " + + key + " was deleted"); } }
To delete key 0 The key 0 was deleted To delete key 2 The key 2 was deleted To delete key 4 The key 4 was deleted To delete key 6 The key 6 was deleted To delete key 8 The key 8 was deleted map size = 5 1 = value1 3 = value3 5 = value5 7 = value7 9 = value9
分析原因
其實上面的三種遍歷方式從根本上講都是使用的叠代器,之所以出現不同的結果是由於remove操作的實現不同決定的。
首先前兩種方法都在調用nextEntry方法的同一個地方拋出了異常
final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); Entry<K,V> e = next; ... ... }
這裏modCount是表示map中的元素被修改了幾次(在移除,新加元素時此值都會自增),而expectedModCount是表示期望的修改次數,在叠代器構造的時候這兩個值是相等,如果在遍歷過程中這兩個值出現了不同步就會拋出ConcurrentModificationException異常。
1、HashMap的remove方法實現
public V remove(Object key) { Entry<K,V> e = 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方法實現
public void remove() { if (current == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); Object k = 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方式不會拋異常。
final Entry<K,V> removeEntryForKey(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
發散
1、如果是遍歷過程中增加或修改數據呢?
增加或修改數據只能通過Map的put方法實現,在遍歷過程中修改數據可以,但如果增加新key就會在下次循環時拋異常,因為在添加新key時modCount也會自增。
2、有些集合類也有同樣的遍歷問題,如ArrayList,通過Iterator方式可正確遍歷完成remove操作,直接調用list的remove方法就會拋異常。
//會拋ConcurrentModificationException異常 for(String str : list){ list.remove(str); } //正確遍歷移除方式 Iterator<String> 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就遍歷了一個已移除的過期數據。
Java遍歷HashMap並修改(remove)(轉載)