ArrayList 迭代器学习笔记
List<String> list = new ArrayList<>(); list.add("str1"); list.add("str2"); list.add("str3"); for (String s : list) { if ("str1".equals(s)) { list.remove(s); } }
这段代码看起来好像没有什么问题,但是如果我们运行,就会抛出ConcurrentModificationException异常。
其实这不是特例,每当我们使用迭代器遍历元素时,如果修改了元素内容(添加、删除元素),就会抛出异常,由于 foreach 同样使用的是迭代器,所以也有同样的情况,大家可以自行试一下。我们来通过源码探究一下这个现象的根本原因。
ArrayList 源码阅读
下面是 ArrayList 的部分源码,可以明显的看到共有两个remove()
方法,一个属于 ArrayList 本身,还有一个属于其内部类 Itr。
public class ArrayList<E> { void remove() { modCount++; // 继承自AbstractList的属性,保存对其中元素的修改次数,每次增加或删除时加1 // 具体删除操作代码 //... } public Iterator<E> iterator() { return new Itr(); } private class Itr implements Iterator<E> { int cursor; // index of next element to return // 在创建迭代器时将当前ArrayList的修改次数赋值给 expectedModCount 保存 int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { // 检查当前所在的 ArrayList 的 modCount 是否与创建 Itr 时的值一致, // 也就是判断获取了Itr迭代器后 ArrayList 中的元素是否被 Itr 外部的方法改变过。 checkForComodification(); // 具体的获取下一个元素的代码 // ... } public void remove() { if (lastRet < 0) throw new IllegalStateException(); // 同 next 中的 checkForComodification 方法 checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; // Itr 内部的删除元素操作,会更新 expectedModCount 值,而外部的则不会 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } }
ArrayList 类中有一个 modCount 属性,这个属性是继承子AbstractList,其保存了我们对 ArrayList 进行的的操作次数,当我们添加或者删除元素时,modeCount 都会进行对应次数的增加。
迭代器迭代时外部方法添加或删除元素
在我们使用 ArrayLis 的 iterator()
方法获取到迭代器进行遍历时,会把 ArrayList 当前状态下的 modCount 赋值给 Itr 类的 expectedModeCount 属性。如果我们在迭代过程中,使用了 ArrayList 的 remove()
或add()
方法,这时 modCount 就会加 1 ,但是迭代器中的expectedModeCount 并没有变化,当我们再使用迭代器的next()
方法时,它会调用checkForComodification()
方法,即
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
发现现在的 modCount 已经与 expectedModCount 不一致了,则会抛出ConcurrentModificationException
异常。
迭代器迭代时,使用迭代器的方法添加或删除元素
但是如果我们使用迭代器提供的remove()
方法,由于其有一个操作:expectedModCount = modCount;
,会修改expectedModCount 的值,所以就不会存在上述问题。
总结
当我们使用迭代器迭代对象的时候,不要使用迭代器之外的方法修改元素,否则会报异常。如果我们要在迭代器迭代时进行修改,可以使用迭代器提供的删除等方法。或者使用其他方法遍历修改。
注意:
特殊情况下在迭代器过程中使用 ArrayList 的删除方法不会报异常,就是 只删除倒数第二个元素的时候,代码如下:
List<String> list = new ArrayList<>(); list.add("str1"); list.add("str2"); list.add("str3"); for (String s : list) { if ("str2".equals(s)) { // 必须只能是倒数第二个元素,这样才不会抛异常 list.remove(s); } }
其原因是迭代器的hasNext()
方法:
public boolean hasNext() { return cursor != size; }
在只删除了倒数第二个元素的时候,cursor 会与 size 相等,这样hasNext()
方法会返回 false ,结束迭代,也就不会进入 next()
方法中,进而执行checkForComodification()
检查方法抛出异常。