迭代地图并更改值时如何避免ConcurrentModificationException?

时间:2010-11-02 14:12:57

标签: java map loops

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

我想迭代这个地图并改变POJO中的一些数据。

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

这不行,因为在迭代它时你不应该修改地图(方法是同步的,但仍然出现ConcurrentModificationException)

我的问题是,如果我需要迭代地图并更改值,我可以使用哪些最佳做法/方法?要创建一个单独的地图并按照我的方式构建,然后返回副本?

7 个答案:

答案 0 :(得分:21)

两个选项:

选项1

  

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

您是否正在将参考更改为POJO?例如,入口指向其他东西完全?因为如果没有,根本不需要将它从地图中删除,你可以改变它。

选项2

如果需要实际更改对POJO的引用(例如,条目的值),您仍然可以通过迭代Map.Entry实例来实现entrySet()。您可以在条目上使用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());
}

输出(无特定顺序)为:

  

拜访两人   
修改它   
参观一个   
访问三   
2 = DUE   
一个=乌诺   
3 = TRE

...没有任何修改例外。你可能想要同步这个,以防其他人也在查看/删除该条目。

答案 1 :(得分:15)

Map进行迭代并同时添加条目将导致大多数ConcurrentModificationExceptionMap。对于没有的Map类(例如ConcurrentHashMap),无法保证迭代将访问所有条目。

根据您正在做的事情,您可以在迭代时执行以下操作:

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

对于其他类型的更改,您可能需要:

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

最后,Google Collections和Apache Commons Collections库提供了用于“转换”地图的实用程序类。

答案 2 :(得分:8)

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

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

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

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

答案 3 :(得分:3)

创建一个新地图(mapNew)。然后遍历现有映射(mapOld),并将所有已更改和已转换的条目添加到mapNew中。迭代完成后,将mapNew中的所有值都放到mapOld中。如果数据量很大,这可能不够好。

或者只使用Google collections - 他们有Maps.transformValues()Maps.transformEntries()

答案 4 :(得分:2)

为了提供正确的答案,你应该多解释一下,你想要实现的目标。

仍然有一些(可能有用的)建议:

  • 让您的POJO线程安全,并直接在POJO上进行数据更新。然后你不需要操纵地图。
  • 使用ConcurrentHashMap
  • 继续使用简单HashMap ,但在每次修改时构建一个新地图并在后台切换地图(同步切换操作或使用AtomicReference

哪种方法最好在很大程度上取决于您的应用,很难给您任何“最佳实践”。与往常一样,使用真实数据制作您自己的基准

答案 5 :(得分:0)

尝试使用ConcurrentHashMap

来自JavaDoc,

  

支持完整的哈希表   检索和并发   可调节的预期并发性   更新。

要发生ConcurrentModificationException,通常是:

  

通常不允许这样做   一个线程来修改Collection   而另一个线程正在迭代   它

答案 6 :(得分:0)

另一种有点折磨的方法是使用java.util.concurrent.atomic.AtomicReference作为地图的值类型。在您的情况下,这意味着声明您的地图类型

Map<String, AtomicReference<POJO>>

您当然不需要引用的 atomic 性质,但这是一种廉价的方法,可以使值槽可重新绑定,而无需通过{{1}替换整个Map.Entry }。

尽管如此,在阅读了其他一些回复之后,我也建议使用Map.Entry#setValue(),这是我从未需要也没有注意到的。直到今天。