如何在迭代Map且修改值时避免ConcurrentModificationException?

23

我有一个包含键(字符串)和值(POJO)的地图。

我想遍历这个地图并更改一些POJO中的数据。

我继承的当前代码删除了给定的条目,并在对POJO进行一些更改后重新添加它。

这不太好,因为您不应该在迭代地图时修改它(方法是同步的,但仍可能出现ConcurrentModificationException)。

我的问题是,如果我需要遍历地图并更改值,那么我可以使用哪些最佳实践/方法来做到这一点?创建一个单独的映射并随着遍历而构建它,然后返回该副本?


修改 map 中的条目是否需要先将其删除? - Jeremy
@Jeremy,POJO本身需要更改其状态,因此应该能够保留在映射中。 - Jimmy
2
如果您没有删除任何值,那么就不应该出现“ConcurrentModificationException”。只要修改值的状态而没有外部因素干扰,这就不是问题。 - Jeremy
3
指出术语有点令人困惑还挺有趣的,“ConcurrentModificationException”(简称“CoMo”)让人觉得有并发问题,尽管只有一个线程访问数据结构时也可能会发生CoMo。 :) - SyntaxT3rr0r
7个回答

24

两个选项:

选项1

我继承的当前代码会将给定的条目移除,然后在对POJO进行一些更改后将其重新添加回去。

你是否正在更改POJO的引用?例如,使得该条目指向完全不同的东西?如果没有,那么根本没有必要从地图中删除它,你可以直接更改。

选项2

如果你确实需要更改POJO的引用(例如,条目的值),你仍然可以通过迭代来就地修改entrySet()中的Map.Entry。你可以在条目上使用setValue,这不会修改你正在遍历的内容。

例如:

Map<String,String>                  map;
Map.Entry<String,String>            entry;
Iterator<Map.Entry<String,String>>  it;

// Create the map
map = new HashMap<String,String>();
map.put("one", "uno");
map.put("two", "due");
map.put("three", "tre");

// Iterate through the entries, changing one of them
it = map.entrySet().iterator();
while (it.hasNext())
{
    entry = it.next();
    System.out.println("Visiting " + entry.getKey());
    if (entry.getKey().equals("two"))
    {
        System.out.println("Modifying it");
        entry.setValue("DUE");
    }
}

// Show the result
it = map.entrySet().iterator();
while (it.hasNext())
{
    entry = it.next();
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

输出结果(没有特定顺序)为:

访问two
修改它
访问one
访问three
two=DUE
one=uno
three=tre

...没有任何修改异常。您可能希望在这种情况下对其进行同步,以防其他内容也在查看/更改该条目。


非常好的回答。只是补充一下,这是有关迭代器的代码:http://www.docjar.com/html/api/java/util/HashMap.java.html 滚动到第94行,其中声明了AbstractMapIterator类。 - Jeremy

18

同时迭代一个Map并添加元素通常会导致大多数Map类抛出ConcurrentModificationException异常。而对于不会抛出异常的Map类(例如ConcurrentHashMap),不能保证所有条目都能被迭代。

根据你的具体需求,你可以在迭代时执行以下操作:

  • 使用Iterator.remove()方法删除当前条目,或者
  • 使用Map.Entry.setValue()方法修改当前条目的值。

对于其他类型的更改,您可能需要执行以下操作:

  • 从当前Map中的条目创建一个新的Map,或者
  • 构建一个包含要进行更改的单独数据结构,然后将其应用到Map上。

最后,Google Collections和Apache Commons Collections库具有“转换”地图的实用程序类。


8

为此,您应该使用地图公开的集合视图:

  • keySet() 允许您迭代键。这对您没有帮助,因为键通常是不可变的。
  • values() 是您需要的,如果您只想访问地图值。如果它们是可变对象,则可以直接更改,无需将它们放回地图中。
  • entrySet() 是最强大的版本,允许您直接更改条目的值。

示例:将所有包含下划线的键的值转换为大写

for(Map.Entry<String, String> entry:map.entrySet()){
    if(entry.getKey().contains("_"))
        entry.setValue(entry.getValue().toUpperCase());
}

实际上,如果你只想编辑值对象,使用values集合进行操作即可。假设你的映射类型为<String, Object>:

for(Object o: map.values()){
    if(o instanceof MyBean){
        ((Mybean)o).doStuff();
    }
}

entrySet()不允许更改条目的键,只能更改其值。 - Robocide
1
真的。有人花了9年时间才发现那个错误 :-) - Sean Patrick Floyd

3
创建一个新地图(mapNew)。然后遍历现有地图(mapOld),并将所有已更改和转换的条目添加到mapNew中。迭代完成后,将mapNew中的所有值放入mapOld中。但如果数据量很大,则可能不够好。
或者只需使用Google collections - 它们具有Maps.transformValues()Maps.transformEntries()

2
为了提供正确的答案,您应该解释一下您想要实现什么。
以下是一些(可能有用的)建议:
  • 使您的POJO线程安全并直接在POJO上进行数据更新。然后您就不需要操作map了。
  • 使用ConcurrentHashMap
  • 继续使用简单的HashMap,但在每次修改时构建一个新的map,并在幕后切换maps(同步切换操作或使用AtomicReference
哪种方法最好取决于您的应用程序,很难给出任何“最佳实践”。像往常一样,使用真实数据进行自己的基准测试

0
另一种有点费力的方法是将 java.util.concurrent.atomic.AtomicReference 用作您地图的值类型。在您的情况下,这意味着声明您地图的类型。
Map<String, AtomicReference<POJO>>

你确实不需要引用的原子特性,但这是一种廉价的方法,可以使值插槽可重新绑定,而无需通过Map#put()替换整个Map.Entry

尽管如此,在阅读了这里的其他回答后,我也建议使用{{link1:Map.Entry#setValue()}},直到今天我都没有需要或注意到它。


0

尝试使用 ConcurrentHashMap

从JavaDoc:

支持全检索并可调整更新的期望并发性的哈希表。

一般来说,要发生ConcurrentModificationException异常:

一个线程在迭代集合时修改集合是不被允许的。


这不是重点,不需要并发访问。OP 应该使用 entrySet() 或 values()。 - Sean Patrick Floyd
@seanizer,哦,抱歉,我误解了问题。我只是认为他可以将他的map简单地包装在ConcurrentHashMap中。 - Buhake Sindi

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