我能否在迭代时使用keySet修改HashMap?

3

我知道在迭代过程中不应该修改集合。因此,我们应该使用解决方法。

我有一段代码:

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文档说明map的更改会反映在返回的集合中,反之亦然。这是否意味着先前的代码是不可接受的?


1
运行这段代码会发生什么?你提出问题的目的是什么,考验我们吗? - Abhijit Sarkar
我更喜欢将所有应该被删除的键放入一个集合中,在循环结束后,再遍历该集合并从映射中删除这些键。 - Ralf Renz
你可以在这里找到答案 https://dev59.com/iG025IYBdhLWcg3wSkAq - kimy82
@Nimtar,“Map”没有“add()”方法。我想你应该写“put()”。 - davidxxx
@davidxxx,是的,这是我的错。我无法使用“复制和粘贴”,所以出现了错误。谢谢。 - Nimtar
显示剩余2条评论
3个回答

7
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());
        }
    }

在遍历过程中修改Map,很可能会抛出ConcurrentModificationException异常。

如果遍历的集合支持remove操作,那么可以在迭代视图集合时从Map中删除条目。HashMap的视图集合迭代器支持此操作。同样,使用在Map的entrySet上进行迭代时获取到的Map.Entry实例的setValue方法,可以设置特定映射条目(键值对)的值。因此,在单次迭代内即可完成所需操作,无需使用临时Map。以下是实现代码:

    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
在这种情况下,使用Entry.setValue()是正确的选择,因为实际代码并没有添加新元素,而是修改了映射中的一个元素。 - davidxxx

4

HashMap.keySet()方法更加精确地说明:

集合由映射支持,因此对映射的更改会反映在集合中,反之亦然。

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

keySet()迭代期间,应该谨慎防止修改Map:

如果在集合上进行迭代时修改了Map(除了通过迭代器自身的remove操作),则迭代的结果是未定义的。

您可以按以下方式删除Map的条目:

集合支持元素删除,可以通过Iterator.remove、Set.remove、removeAll、retainAll和clear操作从Map中删除相应的映射。

但是你不能在其中添加条目:

它不支持add或addAll操作。

因此,在 keySet() 迭代器使用时,请使用 Set.remove() 或更简单地迭代 keySetIterator 并调用 Iterator.remove() 从映射中删除元素。
您可以在临时 Map 中添加新元素,然后在迭代后使用该 Map 填充原始 Map。

例如:

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);

编辑: 请注意,如果您想修改地图中的现有条目,则应使用Stuart Marks答案中所述的Map.EntrySet
在其他情况下,需要使用中介Map或创建新Map的Stream


如果您正在迭代,那么只能使用Iterator.remove()。否则,您将会得到一个ConcurrentModificationException异常。 - user207421

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();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接