使用带有地图键集的流时出现 ConcurrentModificationException
ConcurrentModificationException when using stream with Maps key set
我想从 someMap
中删除 someList
中不存在的所有项。看看我的代码:
someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove);
我收到了java.util.ConcurrentModificationException
。为什么?流不是并行的。最优雅的方法是什么?
您不需要 Stream
API。在 keySet
上使用 retainAll
。 keySet()
返回的 Set
的任何更改都反映在原始 Map
中。
someMap.keySet().retainAll(someList);
您的流调用(逻辑上)与以下内容相同:
for (K k : someMap.keySet()) {
if (!someList.contains(k)) {
someMap.remove(k);
}
}
如果你 运行 这个,你会发现它抛出 ConcurrentModificationException
,因为它在你迭代地图的同时修改地图。如果您查看 docs,您会注意到以下内容:
Note that this exception does not always indicate that an object has been concurrently modified by a different thread. If a single thread issues a sequence of method invocations that violates the contract of an object, the object may throw this exception. For example, if a thread modifies a collection directly while it is iterating over the collection with a fail-fast iterator, the iterator will throw this exception.
这就是您正在做的事情,您正在使用的地图实现显然具有快速失败迭代器,因此将抛出此异常。
一种可能的替代方法是直接使用迭代器删除项目:
for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); ) {
K next = ks.next();
if (!someList.contains(k)) {
ks.remove();
}
}
@Eran 已经 如何更好地解决这个问题。我将解释为什么会出现 ConcurrentModificationException
。
出现ConcurrentModificationException
是因为您正在修改流源。您的 Map
很可能是 HashMap
或 TreeMap
或其他非并发映射。假设它是 HashMap
。每个流都由 Spliterator
支持。如果 spliterator 没有 IMMUTABLE
和 CONCURRENT
特征,那么,如文档所述:
After binding a Spliterator should, on a best-effort basis, throw ConcurrentModificationException
if structural interference is detected. Spliterators that do this are called fail-fast.
所以HashMap.keySet().spliterator()
不是IMMUTABLE
(因为这个Set
可以修改)也不是CONCURRENT
(并发更新对HashMap
是不安全的) .所以它只是检测并发更改并抛出一个 ConcurrentModificationException
作为 spliterator 文档规定。
还值得引用 HashMap
文档:
The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a ConcurrentModificationException
. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException
on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
虽然它只提到了迭代器,但我相信拆分器也是一样的。
稍后回答,但您可以将收集器插入您的管道中,以便 forEach 在包含密钥副本的 Set 上运行:
someMap.keySet()
.stream()
.filter(v -> !someList.contains(v))
.collect(Collectors.toSet())
.forEach(someMap::remove);
我想从 someMap
中删除 someList
中不存在的所有项。看看我的代码:
someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove);
我收到了java.util.ConcurrentModificationException
。为什么?流不是并行的。最优雅的方法是什么?
您不需要 Stream
API。在 keySet
上使用 retainAll
。 keySet()
返回的 Set
的任何更改都反映在原始 Map
中。
someMap.keySet().retainAll(someList);
您的流调用(逻辑上)与以下内容相同:
for (K k : someMap.keySet()) {
if (!someList.contains(k)) {
someMap.remove(k);
}
}
如果你 运行 这个,你会发现它抛出 ConcurrentModificationException
,因为它在你迭代地图的同时修改地图。如果您查看 docs,您会注意到以下内容:
Note that this exception does not always indicate that an object has been concurrently modified by a different thread. If a single thread issues a sequence of method invocations that violates the contract of an object, the object may throw this exception. For example, if a thread modifies a collection directly while it is iterating over the collection with a fail-fast iterator, the iterator will throw this exception.
这就是您正在做的事情,您正在使用的地图实现显然具有快速失败迭代器,因此将抛出此异常。
一种可能的替代方法是直接使用迭代器删除项目:
for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); ) {
K next = ks.next();
if (!someList.contains(k)) {
ks.remove();
}
}
@Eran 已经 ConcurrentModificationException
。
出现ConcurrentModificationException
是因为您正在修改流源。您的 Map
很可能是 HashMap
或 TreeMap
或其他非并发映射。假设它是 HashMap
。每个流都由 Spliterator
支持。如果 spliterator 没有 IMMUTABLE
和 CONCURRENT
特征,那么,如文档所述:
After binding a Spliterator should, on a best-effort basis, throw
ConcurrentModificationException
if structural interference is detected. Spliterators that do this are called fail-fast.
所以HashMap.keySet().spliterator()
不是IMMUTABLE
(因为这个Set
可以修改)也不是CONCURRENT
(并发更新对HashMap
是不安全的) .所以它只是检测并发更改并抛出一个 ConcurrentModificationException
作为 spliterator 文档规定。
还值得引用 HashMap
文档:
The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a
ConcurrentModificationException
. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw
ConcurrentModificationException
on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
虽然它只提到了迭代器,但我相信拆分器也是一样的。
稍后回答,但您可以将收集器插入您的管道中,以便 forEach 在包含密钥副本的 Set 上运行:
someMap.keySet()
.stream()
.filter(v -> !someList.contains(v))
.collect(Collectors.toSet())
.forEach(someMap::remove);