01、前言
说起来真特么惭愧:十年 IT 老兵,Java 菜鸟一枚。今天我才了解到 Java 还有 fail-fast 一说。不得不感慨啊,学习真的是没有止境。只要肯学,就会有巨多巨多别人眼中的“旧”知识涌现出来,并且在我这全是新的。
能怎么办呢?除了羞愧,就只能赶紧全身心地投入学习,把这些知识掌握。
为了镇楼,必须搬一段英文来解释一下 fail-fast。
In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system’s state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.
大家不嫌弃的话,我就用蹩脚的英语能力翻译一下。某场战役当中,政委发现司令员在乱指挥的话,就立马报告给权限更高的中央军委——这样可以有效地避免更严重的后果出现。当然了,如果司令员是李云龙的话,报告也没啥用。
不过,Java 的世界里不存在李云龙。fail-fast 扮演的就是政委的角色,一旦报告给上级,后面的行动就别想执行。
怎么和代码关联起来呢?看下面这段代码。
public void test(Wanger wanger) { if (wanger == null) { throw new RuntimeException("wanger 不能为空"); } System.out.println(wanger.toString()); }
一旦检测到 wanger 为 null,就立马抛出异常,让调用者来决定这种情况下该怎么处理,下一步 wanger.toString() 就不会执行了——避免更严重的错误出现,这段代码由于太过简单,体现不出来,后面会讲到。
瞧,fail-fast 就是这个鬼,没什么神秘的。如果大家源码看得比较多的话,这种例子多得就像旅游高峰期的人头。
然后呢,没了?三秒钟,别着急,我们继续。
02、for each 中集合的 remove 操作
很长一段时间里,我都不明白为什么不能在 for each 循环里进行元素的 remove。今天我们就来借机来体验一把。
List<String> list = new ArrayList<>(); list.add("沉默王二"); list.add("沉默王三"); list.add("一个文章真特么有趣的程序员"); for (String str : list) { if ("沉默王二".equals(str)) { list.remove(str); } } System.out.println(list);
这段代码看起来没有任何问题,但运行起来就糟糕了。
Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at com.cmower.java_demo.str.Cmower3.main(Cmower3.java:14)
为毛呢?
03、分析问题的杀手锏
这时候就只能看源码了,ArrayList.java 的 909 行代码是这样的。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
也就是说,remove 的时候执行了 checkForComodification 方法,该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 ConcurrentModificationException 异常。
可为什么会执行 checkForComodification 方法呢?这就需要反编译一下 for each 那段代码了。
List<String> list = new ArrayList(); list.add("沉默王二"); list.add("沉默王三"); list.add("一个文章真特么有趣的程序员"); Iterator var3 = list.iterator(); while (var3.hasNext()) { String str = (String) var3.next(); if ("沉默王二".equals(str)) { list.remove(str); } } System.out.println(list);
原来 for each 是通过迭代器 Iterator 配合 while 循环实现的。
1)ArrayList.iterator() 返回的 Iterator 其实是 ArrayList 的一个内部类 Itr。
public Iterator<E> iterator() {
return new Itr();
}
Itr 实现了 Iterator 接口。
private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } }
也就是说 new Itr() 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 List 的一个成员变量,表示集合被修改的次数。由于 list 此前执行了 3 次 add 方法,所以 modCount 的值为 3;expectedModCount 的值也为 3。
可当执行 list.remove(str) 后,modCount 的值变成了 4。
private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
注:remove 方法内部调用了 fastRemove 方法。
下一次循环执行到 String str = (String) var3.next(); 的时候,就会调用 checkForComodification 方法,此时一个为 3,一个为 4,就只好抛出异常 ConcurrentModificationException 了。
不信,可以直接在 ArrayList 类的 909 行打个断点 debug 一下。
真的耶,一个是 4 一个是 3。
总结一下。在 for each 循环中,集合遍历其实是通过迭代器 Iterator 配合 while 循环实现的,但是元素的 remove 却直接使用的集合类自身的方法。这就导致 Iterator 在遍历的时候,会发现元素在自己不知情的情况下被修改了,它觉得很难接受,就抛出了异常。
读者朋友们,你们是不是觉得我跑题了,fail-fast 和 for each 中集合的 remove 操作有什么关系呢?
有!Iterator 使用了 fail-fast 的保护机制。