为什么要研究这个异常
起因是最近再刷一本《App研发录》的书,其中作者针对这个异常说了一句话”但凡有点编程常识的程序员都应该知道在遍历一个集合时不能删除该集合中的元素”,而我对这句话很不赞同.作为一个有点编程常识的程序员,我觉得在遍历集合时是肯定可以删除元素的,只是删除的方式需要考究.接下来,就针对这个异常进行深入分析,看一下到底应该用神马样子的姿势在集合中删除元素.
需要申明一点: 我看书从来都是带着批判的观点去仔细阅读,对事不对人.
异常举例
以HashMap举例,引起异常的代码如下:
/**
* 探究遍历集合时同时删除元素的case.
*/
public class ConcurrentModificationText {
public static void traverseWithDelete(HashMap<Integer, String> map) {
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
int key = entry.getKey();
if (key % 2 == 0) {
map.remove(key);
//iterator.remove();
} else {
System.out.println("key=" + key + ", value=" + entry.getValue());
}
}
}
public static HashMap<Integer, String> createHashMap(int n) {
HashMap<Integer, String> res = new HashMap<>(n);
for (int i = 1; i <= n; i ++) {
res.put(i, "value=" + i);
}
return res;
}
public static void main(String[] args) {
HashMap<Integer, String> map = createHashMap(10);
traverseWithDelete(map);
}
}
代码运行就会报出:
key=1, value=value=1
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:922)
at java.util.HashMap$EntryIterator.next(HashMap.java:962)
at java.util.HashMap$EntryIterator.next(HashMap.java:960)
at com.wzy.swordoffer.Exception.ConcurrentModificationText.traverseWithDelete(ConcurrentModificationText.java:14)
at com.wzy.swordoffer.Exception.ConcurrentModificationText.main(ConcurrentModificationText.java:36)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Process finished with exit code 1
作者给出一个很麻烦的办法,用一个ArrayList记录要删除的key,然后再遍历这个ArrayList,调用HashMap的remove方法以ArrayList的元素为key进行删除.这种做法有两个缺陷:
- 浪费额外的空间.需要ArrayList记录所有要被删除的key.
- 浪费额外的时间.遍历ArrayList需要耗时.
其实更简单的做法如下,只需要改动一行代码就可以了:
// map.remove(key);
iterator.remove();
源码分析
作为知其然,更要知其所以然的典范,针对这个case,必须从源码的角度去分析一下,为什么在Iterator遍历过程中调用HashMap的remove方法会crash,而调用其自身的remove方法就没问题.
HashMap的Iterator的源码如下:
/**
* HashMap被修改的次数.
*/
transient int modCount;
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K, V> next; // next entry to return
/** 初始化Iterator时,记录当前HashMap被修改的次数 */
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K, V> current; // current entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
final Entry<K, V> nextEntry() {
// 在访问下一个Entry时,判断是否有其他线程对集合进行修改
// 如果有修改,则要尽早报错,也就是所谓的fast-fail,报的错就是ConcurrentModificationException
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K, V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
// 调用HashMap的remove方法
java.util.HashMap.this.removeEntryForKey(k);
// 同步被修改的次数
expectedModCount = modCount;
}
}
已经在关键点上进行注释了,所以不能在iterator遍历时删除元素最根本的原因是HashMap是线程不安全的,所以遇到修改次数不一致时,要尽早报错,也就是fast-fail.
所以,StackOverflow上很多人在解决这个异常时更多的是推荐使用: ConcurrentHashMap,他是线程安全的,就可以从根本上不用考虑修改次数不同步的问题了.