比如说:
for (X x : some_map.values ())
doSomething (x);
,其中doSomething()
通过几层代码间接地为some_map
增加了更多的值。使用迭代器(如上面的示例代码),我将ConcurrentModificationException
扔到我脸上。
我可以使some_map
成为LinkedHashMap
,即具有可预测的迭代顺序。此外,当向其中添加新项时,它总是在迭代顺序的末尾添加。换句话说,如果ConcurrentModificationException
以某种方式没有抛出,循环将只迭代最后新添加的项,即完全可以正常工作。或者,换句话说,我在这里确实有并发修改,但我可以保证它的行为是良好定义的,而不是错误的。
问题:我可以在上面的循环中使用与迭代器"类似"的东西来避免并发修改异常吗?
注意,由于一些额外的约束,我不能使添加的项意识到循环。我也不能把这个改成非地图的东西。这只是一段代码,但some_map
也在其他地方使用,并且是一个映射是有原因的。
我的问题是,我是否可以迭代(不一定以标准方式)我添加项目的相同地图。很明显,我可以迭代一个副本,在循环之后,将副本与原副本进行比较,找到新的项,迭代这些项,以此类推。问题是,我可以完全避免这个,因为在我的情况下,唯一的问题是ConcurrentModificationException
的过度急于抛出。对我来说,回答"不,你做不到"比回答"你能做……"更好。因为我可以自己设计一个替代代码。我只是想知道我是否忽略了一些优雅的解决方案。
我使用的一个技巧是在迭代列表时处理它,当这种处理涉及删除元素时,使用降序索引访问它并在最后删除项。
for(int i=myList.size()-1;i>=0;i--) {
Object item = myList.get(i);
if(needsToBeRemoved(item)) {
myList.remove(i);
}
}
这样就可以在迭代列表的同时操作它。这对列表是有效的,因为除了迭代器之外,还可以通过索引访问列表中的元素。
在处理映射时也可以应用相同的技术,如果它遵循某种排序,在处理(有序映射)时不会改变。
如果你想要或需要使用迭代器,那么除了复制信息(使用第二个map)没有其他选择
:
还可以使用辅助迭代器(键的列表)。例如:
public static void main(String[] args) {
Map<Long, String> map = new HashMap<>();
map.put(1L, "Start");
map.put(10L, "End");
// This throws ConcurrentModificationException
// for (Long value : map.keySet()) {
// map.put(value + 1, "Other");
// }
for (Long value : new ArrayList<Long>(map.keySet())) {
// This works ok
map.put(value + 1, "Other");
}
System.out.println(map);
// Prints: {1=Start, 2=Other, 10=End, 11=Other}
}
这可能有点争议,但是如果您不关心并发修改,为什么不使用LinkedHashMap
而忽略ConcurrentModificationException
呢?
:
try {
myMap.values().forEach(this::doSomething);
}
catch (ConcurrentModificationException ignored) {
}
我认为这将与LinkedHashMap
工作,但它显然不是预期的用途。但是,您可以实现适合此类用途的自己的版本。
"正确"的方法是复制你想要迭代的值或键,然后检查是否添加了一些东西。约:
final Set<K> processedKeys = new HashSet<>();
do {
final Set<K> keysToProcess = new HashSet<>(myMap.keySet());
keysToProcess.removeAll(processedKeys);
keysToProcess.forEach(key -> doSomething(myMap.get(key)));
} while (!keysToProcess.isEmpty());
为@doublep和@RealSkeptic更新 -为什么我认为仅仅忽略myMap.values().forEach(this::doSomething);
中的异常就可以工作。
请参阅forEach
或values()
返回的集合在LinkedHashMap
中的代码:
public final void forEach(Consumer<? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
所以forEach
遍历链表。如果将值添加到map中,则它们将被添加到链表中,因此for
循环将迭代直到到达链表的末尾。该方法首先然后检查修改计数。因此,该操作将有效地应用于所有值,甚至是新添加的值。
迭代索引而不是实际的集合是一种鲜为人知的技巧。您可以使用流API获取第n个元素。我不确定这会有多高效,因为所有的中间对象都被创建了。
LinkedHashMap<String, String> lhm = new LinkedHashMap();
// fill lhm
for ( int idx=0; idx < lhm.size(); idx++ ) {
String val = lhm.values().stream().skip(idx).findFirst().get();
// process val...
}
没有内置的方法,但如果您使用生产者/消费者模式的变体,每次迭代刷新处理队列,则不难:
Map<K, V> map; // assuming
Set<V> processed = new HashSet<>();
while (!map.values().containsAll(processed)) {
List<V> queue = new ArrayList<>(map.values());
queue.removeAll(processed);
V x = queue.get(0);
processed.add(x);
doSomething(x);
}