在Map中迭代并移除元素

303

我正在做:

for (Object key : map.keySet())
    if (something)
        map.remove(key);

原代码抛出了ConcurrentModificationException异常,所以我做了以下更改:

for (Object key : new ArrayList<Object>(map.keySet()))
    if (something)
        map.remove(key);

这个方法以及任何其他修改地图的过程都在同步块中执行。

有更好的解决方案吗?


1
如果这个方法和修改地图的另一个方法都在同步块中,我不明白为什么你需要做任何事情?也许我没有完全理解你的问题?能否请您发布其余的代码? - Amir Afghani
@Raedwald,我认为这个问题及其被接受的答案比其他问题更简洁。 - pstanton
12个回答

411

这里是一个代码示例,可以在for循环中使用迭代器来删除条目。

Map<String, String> map = new HashMap<String, String>() {
  {
    put("test", "test123");
    put("test2", "test456");
  }
};

for(Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); it.hasNext(); ) {
    Map.Entry<String, String> entry = it.next();
    if(entry.getKey().equals("test")) {
        it.remove();
    }
}

24
所以,在循环中你要使用 it.remove() 而不是 collection.remove(key)。这样做很好! - David Gras
12
如果你在使用Java 8,那么elron的回答更可取,但如果是Java 8之前的代码,这个答案也不错。https://dev59.com/z3I-5IYBdhLWcg3wYnOQ#29187813 - KPD
1
在Android上也能很好地运行。 - Apostrofix
1
如果在递归删除元素时向此Map添加了一个元素,它是否仍会抛出ConcurrentModificationException? - Bill Mote
1
@KPD 如果我们在for循环内部还有其他逻辑(就像我的情况一样)那就更好了。 - Optimus Prime
显示剩余2条评论

353

从Java 8开始,您可以按照以下方式执行此操作:

map.entrySet().removeIf(e -> <boolean expression>);

Oracle文档:entrySet()

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


14
请注意,map.values()map.keySet()也支持removeIf() - JJ Brown
5
removeIf在map.values()上的行为是什么?它会删除所有指向该值的键值对元素吗? - Marco Servetto
在 removeIf() 中,我们提供条件,它将删除所有匹配的记录。 - Dharmendrasinh Chudasama

110

使用真正的迭代器。

Iterator<Object> it = map.keySet().iterator();

while (it.hasNext())
{
  it.next();
  if (something)
    it.remove();
 }

实际上,你可能需要迭代 entrySet() 而不是 keySet() 才能使其工作。


3
这个方案看起来比我遍历条目集合的解决方案更优雅。但是很不明显,从键集合中删除会从地图中删除内容(即键集可以是副本)。 - Gennadiy
11
很抱歉再次留言。我确认了从键集中删除确实会从映射中删除,虽然不像从条目集中删除那么明显。 - Gennadiy
1
迭代键集是完成它的方法。 - Tom Hawtin - tackline
8
在调用remove之前,必须先调用Iterator.next()或在这种情况下调用it.next(),否则会抛出IllegalStateException异常。 - H2ONaCl
7
记录一下,这个方法同样适用于字典(map)的值:Iterator it = map.values().iterator(),然后使用it.remove()可以删除它所在的条目。 - olafure
显示剩余5条评论

50

有更好的解决方案吗?

确实有一种更好的方法可以在一个语句中完成,但这取决于基于哪些条件删除元素。

例如:如果要删除所有valuetest的元素,请使用以下方法:

map.values().removeAll(Collections.singleton("test"));

更新 使用 Java 8 中的 Lambda 表达式可以在一行代码中完成。

map.entrySet().removeIf(e-> <boolean expression> );

我知道这个问题太旧了,但更新做事情的更好方式没有任何伤害 :)


1
这可能是一个老问题,但它确实帮了我很大的忙。我以前从未听说过Collections.singleton(),也不知道你可以通过在values()上调用removeAll()来删除map中的元素!谢谢。 - Paul Boddington
2
删除键为"test"的元素: map.keySet().removeAll(Collections.singleton("test")); - Marouane Lakhal
1
@MarouaneLakhal 如果要删除键为test的元素,你只需要执行map.remove("test");就可以了吧? - dzeikei
1
@dzeikei 正如问题中所述,当他在循环遍历map.keySet()时,map.remove(key) 抛出了 ConcurrentModificationException 异常。 - Marouane Lakhal
1
@MarouaneLakhal 如果你已经知道要删除的映射元素的键,为什么还要首先进行循环?在映射中只能有一个具有相同键的条目。 - dzeikei

25

ConcurrentHashMap

你可以使用java.util.concurrent.ConcurrentHashMap

它实现了ConcurrentMap(它扩展了Map接口)。

例如:

Map<Object, Content> map = new ConcurrentHashMap<Object, Content>();

for (Object key : map.keySet()) {
    if (something) {
        map.remove(key);
    }
}

这种方法不会对您的代码进行修改,只有 map 类型会有所不同。


9
这种方法的问题在于ConcurrentHashMap不允许值(或键)为“null”,所以如果你碰巧处理包含null的映射,就不能使用这种方法。 - fiacobelli

9
Java 8 支持更加声明式的迭代方式,我们可以指定想要的结果而不是如何计算。新方法的优点在于它更易读、更少出错。
public static void mapRemove() {

    Map<Integer, String> map = new HashMap<Integer, String>() {
        {
            put(1, "one");
            put(2, "two");
            put(3, "three");
        }
    };

    map.forEach( (key, value) -> { 
        System.out.println( "Key: " + key + "\t" + " Value: " + value );  
    }); 

    map.keySet().removeIf(e->(e>2)); // <-- remove here

    System.out.println("After removing element");

    map.forEach( (key, value) -> { 
        System.out.println( "Key: " + key + "\t" + " Value: " + value ); 
    });
}

以下是结果:
Key: 1   Value: one
Key: 2   Value: two
Key: 3   Value: three
After removing element
Key: 1   Value: one
Key: 2   Value: two

5

在遍历一个映射表时,你必须使用 Iterator 来安全地删除元素。


4

我同意Paul Tomblin的观点。通常我使用键集合的迭代器,然后基于该键的值来设置条件:

Iterator<Integer> it = map.keySet().iterator();
while(it.hasNext()) {
    Integer key = it.next();
    Object val = map.get(key);
    if (val.shouldBeRemoved()) {
        it.remove();
    }
}

10
你应该使用entrySet而不是每次使用keySet并进行get操作。FindBugs甚至为此提供了一个检测器:http://findbugs.sourceforge.net/bugDescriptions.html#WMI_WRONG_MAP_ITERATOR - Tim Büthe

2

另一种更冗长的方法

List<SomeObject> toRemove = new ArrayList<SomeObject>();
for (SomeObject key: map.keySet()) {
    if (something) {
        toRemove.add(key);
    }
}

for (SomeObject key: toRemove) {
    map.remove(key);
}

2

这也应该能够正常工作。

ConcurrentMap<Integer, String> running = ... create and populate map

Set<Entry<Integer, String>> set = running.entrySet();    

for (Entry<Integer, String> entry : set)
{ 
  if (entry.getKey()>600000)
  {
    set.remove(entry.getKey());    
  }
}

应该是这样的:running.remove(entry.getKey()); - Yetti99

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