正向遍历和反向遍历


前言

之前搜索面试题的时候,出现了一个题:一个ArrayList在循环过程中删除,会不会出问题,为什么?心里想的答案是肯定会有问题但是又不知道是为什么,在搜索到答案后,发现里面其实并不简单,所以专门写篇文章研究一下。

for循环正向删除

先看示例,再解析原因:

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //for循环正向循环删除
  11.  
    for (int i = 0;i < list.size();i++){
  12.  
    if (list.get(i).equals("222")){
  13.  
    list.remove(i);
  14.  
    }
  15.  
    }
  16.  
    System.out.println(Arrays.toString(list.toArray()));
  17.  
    }

运行后,输出结果:

[111, 222, 333, 444, 333]

发现,相邻的字符串“222”没有删除,这是为什么呢?画图解释:

解释:删除元素“222”,当循环到下标为1的元素的的时候,发现此位置上的元素是“222”,此处元素应该删除,根据上图中的元素移动可知,在删除元素后面的所有元素都要向前移动一个位置,那么移动之后,原来下标为2的元素“222”,此时下标为1,这是在i = 1,时的循环操作,在下一次的循环中,i = 2,此时就遗漏了第二个元素“222”。

那么再做下一个测试,删除元素“333”,结果将如何?

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //for循环正向循环删除
  11.  
    for (int i = 0;i < list.size();i++){
  12.  
    if (list.get(i).equals("333")){
  13.  
    list.remove(i);
  14.  
    }
  15.  
    }
  16.  
    System.out.println(Arrays.toString(list.toArray()));
  17.  
    }

运行结果:

[111, 222, 222, 444]

发现,没有问题。原理在上一个测试已经说了,就不再赘述。

总结:for循环正向删除,会遗漏连续重复的元素。

 for循环反向删除

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //for循环反向循环删除
  11.  
    for (int i = list.size() - 1;i >= 0;i--){
  12.  
    if (list.get(i).equals("222")){
  13.  
    list.remove(i);
  14.  
    }
  15.  
    }
  16.  
    System.out.println(Arrays.toString(list.toArray()));
  17.  
    }
 

运行结果:

[111, 333, 444, 333]

 发现,没有问题。还是画图解释:

反向删除的时候,循环遍历完了的元素下标才有可能移动(已经遍历的元素,下标变化了也没有影响),所以没有遍历的下标不会移动,自反向删除会遍历到所有的元素,正向会跳过一些元素。

总结:反向遍历删除,没有问题(单线程)。

反向遍历删除(多线程)

  1.  
    public static void main(String[] args) {
  2.  
    ArrayList<String> list = new ArrayList<String>();
  3.  
    list.add( "111");
  4.  
    list.add( "222");
  5.  
    list.add( "222");
  6.  
    list.add( "333");
  7.  
    list.add( "444");
  8.  
    list.add( "333");
  9.  
     
  10.  
    Thread thread1 = new Thread() {
  11.  
    @Override
  12.  
    public void run() {
  13.  
    remove(list, "111");
  14.  
    try {
  15.  
    Thread.sleep( 1000);
  16.  
    } catch (InterruptedException e) {
  17.  
    e.printStackTrace();
  18.  
    }
  19.  
    }
  20.  
    };
  21.  
    Thread thread2 = new Thread() {
  22.  
    @Override
  23.  
    public void run() {
  24.  
    remove(list, "222");
  25.  
    try {
  26.  
    Thread.sleep( 1000);
  27.  
    } catch (InterruptedException e) {
  28.  
    e.printStackTrace();
  29.  
    }
  30.  
    }
  31.  
    };
  32.  
    Thread thread3 = new Thread() {
  33.  
    @Override
  34.  
    public void run() {
  35.  
    remove(list, "333");
  36.  
    try {
  37.  
    Thread.sleep( 1000);
  38.  
    } catch (InterruptedException e) {
  39.  
    e.printStackTrace();
  40.  
    }
  41.  
    }
  42.  
    };
  43.  
    // 使各个线程处于就绪状态
  44.  
    thread1.start();
  45.  
    thread2.start();
  46.  
    thread3.start();
  47.  
    // 等待前面几个线程完成
  48.  
    try {
  49.  
    thread1.join();
  50.  
    thread2.join();
  51.  
    } catch (InterruptedException e) {
  52.  
    e.printStackTrace();
  53.  
    }
  54.  
     
  55.  
    System.out.println(Arrays.toString(list.toArray()));
  56.  
    }
  57.  
     
  58.  
    public static void remove(ArrayList<String> list, String elem) {
  59.  
    // 普通for循环倒序删除,删除过程中元素向左移动,不影响连续删除
  60.  
    for (int i = list.size() - 1; i >= 0; i--) {
  61.  
    if (list.get(i).equals(elem)) {
  62.  
    list.remove(list.get(i));
  63.  
    }
  64.  
    }
  65.  
    }

 

运行结果:

[444]

总结:多线程反向遍历删除,没有问题。

Iterator循环删除

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //foreach循环删除
  11.  
    Iterator iterator = list.iterator();
  12.  
    while (iterator.hasNext()){
  13.  
    if (iterator.next().equals("222")){
  14.  
    list.remove(iterator.next());
  15.  
    }
  16.  
    }
  17.  
    System.out.println(Arrays.toString(list.toArray()));
  18.  
    }

运行结果:

  1.  
    Exception in thread "main" java.util.ConcurrentModificationException
  2.  
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  3.  
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
  4.  
    at joe.effective.Test.main(Test.java:20)

 这个问题就要借助源码来分析了(JDK1.8):

  1.  
    public E remove(int index) {
  2.  
    rangeCheck(index);
  3.  
     
  4.  
    modCount++;
  5.  
    E oldValue = elementData(index);
  6.  
     
  7.  
    int numMoved = size - index - 1;
  8.  
    if (numMoved > 0)
  9.  
    System.arraycopy(elementData, index+ 1, elementData, index,
  10.  
    numMoved);
  11.  
    elementData[--size] = null; // clear to let GC do its work
  12.  
     
  13.  
    return oldValue;
  14.  
    }
  15.  
     
  16.  
    public boolean remove(Object o) {
  17.  
    if (o == null) {
  18.  
    for (int index = 0; index < size; index++)
  19.  
    if (elementData[index] == null) {
  20.  
    fastRemove(index);
  21.  
    return true;
  22.  
    }
  23.  
    } else {
  24.  
    for (int index = 0; index < size; index++)
  25.  
    if (o.equals(elementData[index])) {
  26.  
    fastRemove(index);
  27.  
    return true;
  28.  
    }
  29.  
    }
  30.  
    return false;
  31.  
    }
  32.  
     
  33.  
    private void fastRemove(int index) {
  34.  
    modCount++;
  35.  
    int numMoved = size - index - 1;
  36.  
    if (numMoved > 0)
  37.  
    System.arraycopy(elementData, index+ 1, elementData, index,
  38.  
    numMoved);
  39.  
    elementData[--size] = null; // clear to let GC do its work
  40.  
    }

可以看出,ArrayList的remove方法,一种是根据下标删除,一种是根据元素删除。

发现即使看了remove方法的源码也不能找到报错的原因,由于我们使用了Iterator迭代器,那么再看看迭代器的源码,果不其然,就发现了问题所在:

  1.  
    private class Itr implements Iterator<E>
  2.  
    private class ListItr extends Itr implements ListIterator<E>
  1.  
    public void remove() {
  2.  
    if (lastRet < 0)
  3.  
    throw new IllegalStateException();
  4.  
    checkForComodification(); // 检查修改次数
  5.  
     
  6.  
    try {
  7.  
    ArrayList. this.remove(lastRet);
  8.  
    cursor = lastRet;
  9.  
    lastRet = - 1;
  10.  
    expectedModCount = modCount;
  11.  
    } catch (IndexOutOfBoundsException ex) {
  12.  
    throw new ConcurrentModificationException();
  13.  
    }
  14.  
    }
  15.  
    final void checkForComodification() {
  16.  
    if (modCount != expectedModCount)
  17.  
    throw new ConcurrentModificationException();
  18.  
    }

Itr和ListItr是ArrayList的两个私有内部类,Itr实现了Iterator接口,ListItr继承了Itr类和实现了ListIterator接口。Itr类中也有一个remove方法,迭代器实际调用的也正是这个remove方法,上述源码也就是这个方法的源码。

由源码的第二段代码可以看出,这个remove方法中调用了ArrayList中的remove方法,在这个方法中我们注意到了expectedModCount变量和modCount变量,modCount在前面的代码中也见到了,它记录了ArrayList修改的次数,而前面的变量expectedModCount,这个变量的初值和modCount是相等的;同时在ArrayList.this.remove(lastRet);代码面前,调用了检查次数的方法checkForComodification(),这个方法做的事情很简单,就是如果expectedModCount和modCount不相等,那么就抛出异常ConcurrentModificationException。

我们在用Iterator循环删除的时候,调用的是ArrayList里面的remove方法,删除元素后modCount会增加,expectedModCount则不变,这样就造成了expectedModCount != modCount,那么就抛出异常了。

再用Iterator中的remove方法来测试:

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
     
  11.  
    Iterator iterator = list.iterator();
  12.  
    while (iterator.hasNext()){
  13.  
    if (iterator.next().equals("222")){
  14.  
    iterator.remove();
  15.  
    }
  16.  
    }
  17.  
    System.out.println(Arrays.toString(list.toArray()));
  18.  
    }
运行结果[111, 333, 444, 333]

发现,删除成功且没有报错。

什么原因呢?我们调用的了Iterator中的迭代器删除元素,在这个方法中有:expectedModCount = modCount这样一句代码,所以当我们每删除一次元素,就同步一次,所以调用checkForComodification()时,就不会报错。如果换到多线程中,这个方法不能保证两个变量修改的一致性,结果具有不确定性,所以不推荐这种方法。

总结:Iterator调用ArrayList的删除方法报错,Iterator调用迭代器自己的删除方法,单线程不会报错,多线程会报错。

forEach循环删除

  1.  
    public static void main(String[] args){
  2.  
    List<String> list = new ArrayList<String>();
  3.  
     
  4.  
    list.add( "111");
  5.  
    list.add( "222");
  6.  
    list.add( "222");
  7.  
    list.add( "333");
  8.  
    list.add( "444");
  9.  
    list.add( "333");
  10.  
    //foreach循环删除
  11.  
    for (String str : list){
  12.  
    if (str.equals("222")){
  13.  
    list.remove(str);
  14.  
    }
  15.  
    }
  16.  
     
  17.  
    System.out.println(Arrays.toString(list.toArray()));
  18.  
    }
  1.  
    运行结果
  2.  
    Exception in thread "main" java.util.ConcurrentModificationException
  1.  
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  2.  
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
  3.  
    at joe.effective.Test.main(Test.java:20)

报错。

foreach原理是因为这些集合类都实现了Iterable接口,该接口中定义了Iterator迭代器的产生方法,并且foreach就是通过Iterable接口在序列中进行移动。也就是说:在编译的时候编译器会自动将对for这个关键字的使用转化为对目标的迭代器的使用

明白了原理就跟上述的Iterator删除调用ArrayList中remove一样了。

总结:forEach循环删除报错。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM