关于面试题“ArrayList循环remove()要用Iterator”的研究

两个月前我在参加一场面试的时候,被问到了ArrayList如何循环删除元素,当时我回答用Iterator,当面试官问为何要用Iterator而不用foreach时,我没有答出来,现在又回想到了这个问题,我以为应该把它搞一搞,因此我就写了一个小的demo并结合阅读源代码来验证了一下。java

下面是我验证的ArrayList循环remove()的4种状况,以及其结果(基于oracle jdk1.8):程序员

//List<Integer> list = new ArrayList<>();
//list.add(1);
//list.add(2);
//list.add(3);
//list.add(4);
//循环remove()的4种状况的代码片断:

//#1
for (Integer integer : list) {
    list.remove(integer);
}

结果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
-----------------------------------------------------------------------------------

//#2
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
    Integer integer = iterator.next();
    list.remove(integer);
}

结果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
-----------------------------------------------------------------------------------


//#3
for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}
System.out.println(list);

结果:
[2, 4]
-----------------------------------------------------------------------------------

//#4
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
    iterator.next();
    iterator.remove();
}
System.out.println(list.size());

结果:(惟一一个获得指望值的)
0复制代码

能够看出来这几种状况只有最后一种是获得预期结果的,其余的要么异常要么得不到预期结果,下面我们一个一个进行分析。面试

#1

//#1
for (Integer integer : list) {
    list.remove(integer);
}

结果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)复制代码

经过异常栈,咱们能够定位是在ArrayList的内部类ItrcheckForComodification方法中爆出了ConcurrentModificationException异常(关于这个异常是怎么回事我们暂且不提)咱们打开ArrayList的源码,定位到901行处:bash

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}复制代码

这个爆出异常的方法实际上就作了一件事,检查modCount != expectedModCount由于知足了这个条件,因此抛出了异常,继续查看modCountexpectedModCount这两个变量,发现modCount是继承自AbstractList的一个属性,这个属性有一大段注释并发

/**
 * The number of times this list has been <i>structurally modified</i>.
 * 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.
 *
 * <p>This field is used by the iterator and list iterator implementation
 * returned by the {@code iterator} and {@code listIterator} methods.
 * If the value of this field changes unexpectedly, the iterator (or list
 * iterator) will throw a {@code ConcurrentModificationException} in
 * response to the {@code next}, {@code remove}, {@code previous},
 * {@code set} or {@code add} operations.  This provides
 * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
 * the face of concurrent modification during iteration.
 *
 * <p><b>Use of this field by subclasses is optional.</b> If a subclass
 * wishes to provide fail-fast iterators (and list iterators), then it
 * merely has to increment this field in its {@code add(int, E)} and
 * {@code remove(int)} methods (and any other methods that it overrides
 * that result in structural modifications to the list).  A single call to
 * {@code add(int, E)} or {@code remove(int)} must add no more than
 * one to this field, or the iterators (and list iterators) will throw
 * bogus {@code ConcurrentModificationExceptions}.  If an implementation
 * does not wish to provide fail-fast iterators, this field may be
 * ignored.
 */
protected transient int modCount = 0;复制代码

大体的意思是这个字段用于有fail-fast行为的子集合类的,用来记录集合被修改过的次数,咱们回到ArrayList能够找到在add(E e)的调用链中的一个方法ensureExplicitCapacity(int minCapacity) 中会对modCount自增:oracle

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}复制代码

咱们在初始化list时调用了4次add(E e)因此如今modCount的值为4ide


再来找 expectedModCount:这个变量是定义在 ArrayListIterator的实现类 Itr中的,它默认被赋值为 modCount


知道了这两个变量是什么了之后,咱们开始走查吧,在 Itr的相关方法中加好断点(编译器会将 foreach编译为使用 Iterator的方式,因此咱们看 Itr就能够了),开始调试:

循环:
优化


在迭代的每次 next()时都会调用 checkForComodification()

list.remove()

ArrayListremove(Object o)中又调用了 fastRemove(index)


fastRemove(index)中对 modCount进行了自增,刚才说过 modCount通过4次 add(E e)初始化后是 4因此 ++后如今是 5

继续往下走,进入下次迭代:ui


又一次执行 next()next()调用 checkForComodification(),这时在上边的过程当中 modCount因为 fastRemove(index)的操做已经变成了 5expectedModCount则没有人动,因此很快就知足了抛出异常的条件 modCount != expectedModCount(也就是前面提到的 fail-fast),程序退出。

#2

//#2
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
    Integer integer = iterator.next();
    list.remove(integer);
}

结果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)复制代码

其实这个#2和#1是同样的,foreach会在编译期被优化为Iterator调用,因此看#1就好啦。this


#3

//#3
for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}
System.out.println(list);

结果:
[2, 4]复制代码

这种一本正经的胡说八道的状况也许在写代码犯困的状况下会出现... 不作文字解释了,用println()来讲明吧:

第0次循环开始
remove(0)前的list: [1, 2, 3, 4]
remove(0)前的list.size()=4
执行了remove(0)
remove(0)后的list.size()=3
remove(0)后的list: [2, 3, 4]
下一次循环的i=1
下一次循环的list.size()=3
第0次循环结束
是否还有条件进入下次循环?: true

第1次循环开始
remove(1)前的list: [2, 3, 4]
remove(1)前的list.size()=3
执行了remove(1)
remove(1)后的list.size()=2
remove(1)后的list: [2, 4]
下一次循环的i=2
下一次循环的list.size()=2
第1次循环结束
是否还有条件进入下次循环?: false


Process finished with exit code 0复制代码

实际上ArrayListItr游标最后一次返回值索引来解决了这种size越删越小,可是要删除元素的index愈来愈大的尴尬局面,这个将在#4里说明。

#4

这个才是正儿八经可以正确执行的方式,用了ArrayList中迭代器Itrremove()而不是用ArrayList自己的remove(),咱们调试一下吧看看到底经历了什么:

迭代:

Itr初始化:游标 cursor = 0; 最后一次返回值索引 lastRet = -1; 指望修改次数 expectedModCount = modCount = 4;


迭代的 hasNext():检查游标是否已经到达当前list的 size,若是没有则说明能够继续迭代:



迭代的 next()checkForComodification() 此时 expectedModCountmodCount是相等的,不会抛出 ConcurrentModificationException,而后取到游标(第一次迭代游标是 0)对应的list的元素,再将游标+1,也就是游标后移指向下一个元素,而后将游标原值 0赋给最后一次返回值索引,也就是最后一次返回的是索引 0对应的元素

iterator.remove():一样checkForComodification()而后调用ArrayListremove(lastRet)删除最后返回的元素,删除后modCount会自增

删除完成后,将游标赋值成最后一次返回值索引,其实也就是将游标回退了一格回到了上一次的位置,而后将最后一次返回值索引从新设置为了初始值-1,最后expectedModCount又从新赋值为了上一步过程完成后新的modCount


由上两个步骤能够看出来,虽然list的 size每次 remove()都会 -1,可是因为每次 remove()都会将游标回退,而后将最后一次返回值索引重置,因此实际上没回 remove()的都是当前集合的第 0个元素,就不会出现#3中 size越删越小,而要删除元素的索引愈来愈大的状况了,同时因为在 remove()过程当中 expectedModCountmodCount始终经过赋值保持相等,因此也不会出现 fail-fast抛出异常的状况了。

以上是我经过走查源码的方式对面试题“ArrayList循环remove()要用Iterator”作的一点研究,没考虑并发场景,这篇文章写了大概3个多小时,写完这篇文章办公室就剩我一我的了,我也该回去了,今天1024程序员节,你们节日快乐!


2017.10.25更新#1

感谢@llearn的提醒,#3也能够用用巧妙的方式来获得正确的结果的(再面试的时候,我以为能够和面试官说不必定要用Iterator了,感谢@llearn

//#3 我以为能够这样
for (int i = 0; i < list.size(); ) {
list.remove(0);
}
System.out.println(list);

2017.10.25更新#2

感谢@ChinLong的提醒,提供了另外一种不用Iterator的方法,也就是倒着循环(这种方案我写完文章时也想到了,但没有本身印证到demo上),感谢@ChinLong

然道就没有人和我一下喜欢倒着删的.听别人说倒着迭代速度稍微快一点???for (int i = list.size() -1; i >= 0; i-- ) { list.remove(i);}System.out.println(list);

相关文章
相关标签/搜索