前言
其实我们在遍历Map
类型的集合的时候,有时候可能希望删除满足一定条件的key
。但是这个时候就会发现,抛异常了。那么本文就以LinkedHashMap
为例,来看下原理。
一. 案例复现
我们来复现一下:
public static void main(String[] args) throws Exception {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(16, 0.75F, true) {{
put(1, 1);
put(2, 2);
put(3, 3);
put(4, 4);
}};
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getKey() % 2 == 0) {
System.out.println("To delete key " + entry.getKey());
map.remove(entry.getKey());
System.out.println("The key " + +entry.getKey() + " was deleted");
}
}
}
结果如下:
这是因为在遍历的时候,通过remove()
函数去删除节点的时候,只会维护modCount
这个变量。而expectedModCount
变量却不会维护,因此造成两个数据不一致。
二. 原理分析
我在这篇文章中讲了关于remove
函数的原理:Java - LinkedHashMap原理分析
里面有这么一段代码:
abstract class HashIterator {
public final void remove() {
Node<K,V> p = current;
// 理论上不可能删除null,是的抛出异常
if (p == null)
throw new IllegalStateException();
// 高并发下很容易造成这种情况,会抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
// 节点删除
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
我们可以看到,关于ConcurrentModificationException
异常的抛出,本质原因就是:HashMap
中在删除一个节点的时候,只会维护modCount
这个变量,而不会维护expectedModCount
变量。
- 因此当程序在成功的删除一个变量的时候,
modCount
就会发生改变。 - 那么下一次循环的时候,就会发现
modCount != expectedModCount
。随即抛出异常。
那么我们只需要再删除节点的时候,同时维护这两个变量就可以了。LinkedHashMap
中有一个迭代器LinkedHashIterator
:
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
我们可以发现,通过迭代器来remove
元素的时候,会同时维护上述两个变量。那么我们是不是可以通过迭代器的方式来代替传统的for
循环呢?
三. 问题解决
解决:我们通过迭代器去删除,最终代码更改为:
public static void main(String[] args) throws Exception {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(16, 0.75F, true) {{
put(1, 1);
put(2, 2);
put(3, 3);
put(4, 4);
}};
Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, Integer> entry = iterator.next();
if (entry.getKey() % 2 == 0) {
System.out.println("To delete key " + entry.getKey());
iterator.remove();
System.out.println("The key " + +entry.getKey() + " was deleted");
}
}
}
结果如下:
可以发现,满足条件的元素能够被删除,并且程序不会抛异常。这个问题也就解决了。
备注:当然,在正常开发中需要避免出现这种循环里面删除元素的操作。