上篇文章 走进 JDK 之 ArrayList(一) 简单分析了 ArrayList 的源码,文末留下了一个问题,modCount
是干啥用的?下面我们通过一个小例子来引出今天的内容。
public static void main(String[] args){ List<String> list= new ArrayList<>(); list.add("java"); list.add("kotlin"); list.add("dart"); for (String s:list){ if (s.equals("dart")) list.remove(s); } } 复制代码
大多数人应该都这么干过,然后得到一个鲜红的 ConcurrentModificationException
,具体错误堆栈信息如下:
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 collection.ArrayListTest.main(ArrayListTest.java:15) 复制代码
报错位置是 ArrayList
的内部类 Itr
中的 checkForComodification()
方法。至于如何调用到这个方法的,我们首先得知道上面的代码中发生了什么。看字节码的话太麻烦了又不容易理解,推荐一个反编译神器 jad
,javac 编译得到 class 文件之后执行如下命令:
./jad ArrayListTest.class 复制代码
得到 ArrayListTest.jad 文件,直接用文本编辑器打开即可:
public class ArrayListTest { public ArrayListTest() { } public static void main(String args[]) { ArrayList arraylist = new ArrayList(); arraylist.add("java"); arraylist.add("kotlin"); arraylist.add("dart"); Iterator iterator = arraylist.iterator(); // 1 do { if(!iterator.hasNext()) // 2 break; String s = (String)iterator.next(); // 3 if(s.equals("dart")) arraylist.remove(s); } while(true); } } 复制代码
从反编译得到的代码我们可以发现,增强型 for 循环只是一个语法糖而已,编译器帮我们进行了处理,其实是调用了迭代器来进行循环。着重看一下上面标注的三句代码,是整个迭代过程的核心。
第一句,获取 ArrayList 的迭代器。
public Iterator<E> iterator() { return new Itr(); } 复制代码
AbstractList
中定义了一个迭代器 Itr
,但是它的子类 ArrayList 并没有直接使用父类的迭代器,而是自己定义了一个优化版本的 Itr
。循环体中第二句代码首先会判断是否 hasNext()
,存在的话调用 next
获取元素,不存在的话跳出循环。增强型 for 循环的基本实现就是这样的。hasNext()
和 next()
方法源码如下:
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; if (i >= size) // 判断是否越界 throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) // 再次判断,如果越界,可能是并发修改导致 throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } ...... // 省略其他代码 } 复制代码
cursur
表示当前游标位置,hasNext()
方法就是根据 cursor 是否等于集合大小 size
判断是否还有下一个元素。成员变量中有个 expectedModCount
,定义如下:
int expectedModCount = modCount; 复制代码
终于发现了 modCount
的踪影,它被赋值给了 expectedModCount
变量,字面意思就是 期望的修改次数
。具体它有什么用,接着看 next()
方法中的第一行代码,调用了 checkForComodification()
方法,这是用来做并发检测的:
final void checkForComodification() { if (modCount != expectedModCount) // 在迭代的过程中 modCount 发生了改变 throw new ConcurrentModificationException(); } 复制代码
异常就是这样抛出来的,modCount
和 expectedModCount
不相等,即实际的修改次数与期望的修改次数不相等。expectedModCount
是在迭代器初始化的过程中赋值的,其值等于 modCount
。在迭代过程中又不相等了,那就只可能是在迭代过程中修改了集合,造成了 modCount
变化。那么,哪些操作会导致 modCount
发生变化呢?JDK 源码注释中做了以下说明(modCount 在 AbstractList 中声明):
The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
集合的结构修改次数。结构修改指的是集合大小的变化。所以只要是涉及到增加或者删除元素的方法,都要改变 modCount
。以 ArrayList 的 remove() 方法为例:
public E remove(int index) { rangeCheck(index); // 边界检测 modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // 移动 index 之后的所有元素 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } 复制代码
通过 modCount++
使其自增 1。
由于 ArrayList 并不是线程安全的,一边迭代一边改变集合,的确可能导致多线程下代码表现不一致。可能有人会有这样的疑问,文章开头的测试代码并没有涉及到并发操作啊,为什么还是抛出了异常?这就是集合的 fail-fast(快速失败)
机制。
fail-fast
错误机制并不保证错误一定会发生,但是当错误发生的时候一定可以抛出异常。它不管你是不是真的并发操作,只要可能是并发操作,就给你提前抛出异常。针对非线程安全的集合类,这是一种健壮的处理方式。但是你如果真的想在单线程中这样操作应该怎么办?没关系,让 modCount
和 expectedModCount
相等就完事了,ArrayList 的迭代器为我们提供了这样的 add()
和 remove()
方法:
public void add(E e) { checkForComodification(); try { int i = cursor; ArrayList.this.add(i, e); // add 之后要修改 modCount cursor = i + 1; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); // remove 之后要修改 modCount cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } 复制代码
上面的代码实现在修改了集合结构之后都会给 expectedModCount
重新赋值,使其与 modCount
相等。修改一下文章开头的测试代码:
public static void main(String[] args){ List<String> list= new ArrayList<>(); list.add("java"); list.add("kotlin"); list.add("dart"); // for (String s:list){ // if (s.equals("dart")) // list.remove(s); // } Iterator<String> iterator=list.iterator(); while (iterator.hasNext()){ String s= iterator.next(); if (s.equals("dart")) iterator.remove(); } } 复制代码
这样就不会再报错了。
最后最后再给你出一道题,仔细看一下:
public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("java"); list.add("kotlin"); list.add("dart"); for (String s : list) { if (s.equals("kotlin")) list.remove(s); } } 复制代码
如果没看出来和文章开头那道题的区别,那就再翻上去仔细观察一下。之前我们要删的是 dart
,集合中的最后一个元素。现在要删的是 kotlin
,集合中的第二个元素。执行结果会怎么样?你要是精通脑筋急转弯的话,肯定能给出正确答案。没错,这次成功删除了元素并且没有任何异常。这是为什么呢?删除 dart
就报异常,删除 kotlin
就没问题,这是歧视 dart
吗。再把迭代器的代码掏出来:
public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); // 并发检测 int i = cursor; if (i >= size) // 判断是否越界 throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) // 再次判断,如果越界,可能是并发修改导致 throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } 复制代码
集合中添加了 3 个元素,所以初始化迭代器之后,expectedModCount = modCount = 3
,cursor
此时为 0 。先来分析文章开头的代码,删除集合中最后一个元素的情况:
- 执行完第一次循环,
cursor
为 1,未产生删除操作,modCount
为 3,expectedModCount
为 3,size
为 3。cursor != size
,hasNext()
判断还有元素。 - 执行完第二次循环,
cursor
为 2,仍未产生删除操作,modCount
为 3,expectedModCount
为 3,size
为 3。cursor != size
,hasNext()
判断还有元素。 - 执行完第三次循环,
cursor
为 3,由于产生删除了操作,modCount
为 4,expectedModCount
仍为 3,size
变为 2。cursor != size
,hasNext()
判断还有元素,继续迭代,其实已经没有元素了。 - 继续迭代,调用
next()
方法,此时expectedModCount != modCount
,直接抛出异常。
循环次数 | cursor | modCount | expectedModCount | size |
1 | 1 | 3 | 3 | 3 |
2 | 2 | 3 | 3 | 3 |
3 | 3 | 4 | 3 | 2 |
再来看看删除 kotlin
的执行流程:
- 执行完第一次循环,
cursor
为 1,未产生删除操作,modCount
为 3,expectedModCount
为 3,size
为 3。cursor != size
,hasNext()
判断还有元素。 - 执行完第二次循环,
cursor
为 2,产生删除操作,modCount
为 4,expectedModCount
为 3,size
为 2。cursor == size
,hasNext()
判断没有元素了,不再调用next()
方法。
并不是 fail-fast
失效了,仅仅只是恰好 cursor == size
,hasNext()
方法误以为集合中已经没有元素了,其实还有一个元素。循环两次之后就终止循环了,不再调用 next()
方法,也就不存在并发检测了。
循环次数 | cursor | modCount | expectedModCount | size |
1 | 1 | 3 | 3 | 3 |
2 | 2 | 3 | 3 | 2 |
本文由一个 ConcurrentModificationException
的例子,顺藤摸瓜,解析了 ArrayList 迭代器的源码,同时说明了 Java 集合框架的 fail-fast
机制。最后也验证了增强型 for 循环中删除元素并不是百分之百会触发 fail-fast
。
ArrayList
就说到这里了,下一篇来看看 List
中同样重要的 LinkedList
。