我可以在迭代期间使用keySet修改HashMap吗?

时间:2018-04-06 07:58:07

标签: java collections iteration

我知道在迭代期间不应该修改集合。所以我们应该有解决方法。

我有一个代码:

Map<Key, Value> map = getMap(); // map generating is hidden
for (Key key : map.keySet()) {
  if (isToRemove(key)) {
    map.remove(key);
  } else {
    map.put(key, getNewValue());
  }
}

是未定义的行为还是有效的代码?

keySet文档sais表示地图的更改会反映在返回的集合中,反之亦然。这是否意味着以前的代码是不可接受的?

3 个答案:

答案 0 :(得分:5)

answer from davidxxx指出地图上的视图集合链接到地图,并且在迭代视图集合时对地图的修改可能是正确的(+1)结果为ConcurrentModificationException。地图上的视图集合由entrySetkeySetvalues方法提供。

因此,原始代码:

Map<Key, Value> map = getMap();
for (Key key : map.keySet()) {
    if (isToRemove(key)) {
        map.remove(key);
    } else {
        map.add(key, getNewValue());
    }
}

很可能会抛出ConcurrentModificationException,因为它会在每次迭代过程中修改地图。

如果该视图集合的迭代器支持remove操作,则可以在迭代视图集合时从地图中删除条目。 HashMap的视图集合的迭代器确实支持这一点。通过使用迭代地图时获得的setValue实例的Map.Entry方法来设置特定地图条目(键值对)的值,可能 ; s entrySet。因此,可以在一次迭代中执行您想要执行的操作,而无需使用临时映射。这是执行此操作的代码:

Map<Key, Value> map = getMap();
for (var entryIterator = map.entrySet().iterator(); entryIterator.hasNext(); ) {
    var entry = entryIterator.next();
    if (isToRemove(entry.getKey())) {
        entryIterator.remove();
    } else {
        entry.setValue(getNewValue());
    }
}

请注意使用Java 10的var构造。如果您不在Java 10上,则必须明确写出类型声明:

Map<Key, Value> map = getMap();
for (Iterator<Map.Entry<Key, Value>> entryIterator = map.entrySet().iterator(); entryIterator.hasNext(); ) {
    Map.Entry<Key, Value> entry = entryIterator.next();
    if (isToRemove(entry.getKey())) {
        entryIterator.remove();
    } else {
        entry.setValue(getNewValue());
    }
}

最后,鉴于这是一个中等复杂的地图操作,使用流来完成工作可能会很有成效。请注意,这会创建一个新地图,而不是就地修改现有地图。

import java.util.Map.Entry;
import static java.util.Map.entry; // requires Java 9

Map<Key, Value> result =
    getMap().entrySet().stream()
            .filter(e -> ! isToRemove(e.getKey()))
            .map(e -> entry(e.getKey(), getNewValue()))
            .collect(toMap(Entry::getKey, Entry::getValue));

答案 1 :(得分:3)

HashMap.keySet()方法更准确地说明了:

  

该集由地图支持,因此对地图的更改将反映在中   集合,反之亦然。

这意味着keySet()返回的元素和Map的键引用相同的对象。因此,当然更改Set的任何元素的状态(例如key.setFoo(new Foo());)将反映在Map键中,反之亦然。

您应该谨慎并防止在keyset()次迭代期间修改地图:

  

如果在对集合进行迭代时修改了地图   (除了通过迭代器自己的删除操作),结果   迭代是未定义的

您可以删除地图条目:

  

该集支持删除元素,删除相应的元素   从地图映射,通过Iterator.remove,Set.remove,removeAll,   retainAll,并清除操作。

但您无法在以下位置添加条目:

  

它不支持add或addAll操作。

总而言之,在keySet()迭代器使用Set.remove()或更多时,只需使用Iterator的{​​{1}}进行迭代,然后调用keySet即可从中删除元素地图。
您可以在迭代后使用的临时Map中添加新元素以填充原始Map。

例如:

Iterator.remove()

修改: 请注意,由于您要修改地图的现有条目,因此应使用Stuart Marks answer中所述的Map<Key, Value> map = getMap(); // map generating is hidden Map<Key, Value> tempMap = new HashMap<>(); for (Iterator<Key> keyIterator = map.keySet().iterator(); keyIterator.hasNext();) { Key key = keyIterator.next(); if (isToRemove(key)) { keyIterator.remove(); } else { tempMap.put(key, getNewValue()); } } map.putAll(tempMap); 。 在其他情况下,需要使用创建新地图的中介Map.EntrySetMap

答案 2 :(得分:1)

如果您运行代码,则会获得ConcurrentModificationException。以下是使用迭代器而不是密钥集或等效的Java8 +功能API的方法:

Map<String, Object> bag = new LinkedHashMap<>();
bag.put("Foo", 1);
bag.put("Bar", "Hooray");

// Throws ConcurrentModificationException
for (Map.Entry<String, Object> e : bag.entrySet()) {
    if (e.getKey().equals("Foo")) {
        bag.remove(e.getKey());
    }
}

// Since Java 8
bag.keySet().removeIf(key -> key.equals("Foo"));

// Until Java 7
Iterator<String> it = bag.keySet().iterator();
while (it.hasNext()) {
    if (it.next().equals("Bar")) {
        it.remove();
    }
}
相关问题