1. 程式人生 > >Java遍歷HashMap並修改(remove)(轉載)

Java遍歷HashMap並修改(remove)(轉載)

value 輸出 AI 中修改 快速 lec dex 直接 was

遍歷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)(轉載)