• ArrayList在循环过程中删除中出现的问题


    前言

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

    for循环正向删除

    先看示例,再解析原因:

    public static void main(String[] args){
            List<String> list = new ArrayList<String>();
    
            list.add("111");
            list.add("222");
            list.add("222");
            list.add("333");
            list.add("444");
            list.add("333");
            //for循环正向循环删除
            for (int i = 0;i < list.size();i++){
                if (list.get(i).equals("222")){
                    list.remove(i);
                }
            }
            System.out.println(Arrays.toString(list.toArray()));
        }

    运行后,输出结果:

    [111, 222, 333, 444, 333]

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

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

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

    public static void main(String[] args){
            List<String> list = new ArrayList<String>();
    
            list.add("111");
            list.add("222");
            list.add("222");
            list.add("333");
            list.add("444");
            list.add("333");
            //for循环正向循环删除
            for (int i = 0;i < list.size();i++){
                if (list.get(i).equals("333")){
                    list.remove(i);
                }
            }
            System.out.println(Arrays.toString(list.toArray()));
        }

    运行结果:

    [111, 222, 222, 444]

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

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

     for循环反向删除

    public static void main(String[] args){
            List<String> list = new ArrayList<String>();
    
            list.add("111");
            list.add("222");
            list.add("222");
            list.add("333");
            list.add("444");
            list.add("333");
            //for循环反向循环删除
            for (int i = list.size() - 1;i >= 0;i--){
                if (list.get(i).equals("222")){
                    list.remove(i);
                }
            }
            System.out.println(Arrays.toString(list.toArray()));
        }

    运行结果:

    [111, 333, 444, 333]

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

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

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

    反向遍历删除(多线程)

    public static void main(String[] args) {
                    ArrayList<String> list = new ArrayList<String>();
                    list.add("111");
                    list.add("222");
                    list.add("222");
                    list.add("333");
                    list.add("444");
                    list.add("333");
    
                    Thread thread1 = new Thread() {
                        @Override
                        public void run() {
                            remove(list,"111");
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    };
                    Thread thread2 = new Thread() {
                        @Override
                        public void run() {
                            remove(list, "222");
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    };
                    Thread thread3 = new Thread() {
                        @Override
                        public void run() {
                            remove(list, "333");
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    };
                    // 使各个线程处于就绪状态
                    thread1.start();
                    thread2.start();
                    thread3.start();
                    // 等待前面几个线程完成
                    try {
                        thread1.join();
                        thread2.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    System.out.println(Arrays.toString(list.toArray()));
                }
    
                public static void remove(ArrayList<String> list, String elem) {
                    // 普通for循环倒序删除,删除过程中元素向左移动,不影响连续删除
                    for (int i = list.size() - 1; i >= 0; i--) {
                        if (list.get(i).equals(elem)) {
                            list.remove(list.get(i));
                        }
                    }
                }

    运行结果:

    [444]

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

    Iterator循环删除

    public static void main(String[] args){
            List<String> list = new ArrayList<String>();
    
            list.add("111");
            list.add("222");
            list.add("222");
            list.add("333");
            list.add("444");
            list.add("333");
            //foreach循环删除
            Iterator iterator = list.iterator();
           while (iterator.hasNext()){
               if (iterator.next().equals("222")){
                   list.remove(iterator.next());
               }
           }
            System.out.println(Arrays.toString(list.toArray()));
        }

    运行结果:

    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 joe.effective.Test.main(Test.java:20)

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

    public E remove(int index) {
        rangeCheck(index);
    
        modCount++;
        E oldValue = elementData(index);
    
        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
    
        return oldValue;
    }
    
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
    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
    }

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

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

    private class Itr implements Iterator<E>
    private class ListItr extends Itr implements ListIterator<E> 
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification(); // 检查修改次数
    
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

    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方法来测试:

    public static void main(String[] args){
            List<String> list = new ArrayList<String>();
    
            list.add("111");
            list.add("222");
            list.add("222");
            list.add("333");
            list.add("444");
            list.add("333");
            
            Iterator iterator = list.iterator();
           while (iterator.hasNext()){
               if (iterator.next().equals("222")){
                   iterator.remove();
               }
           }
            System.out.println(Arrays.toString(list.toArray()));
        }

    运行结果:

    [111, 333, 444, 333]

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

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

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

    forEach循环删除

    public static void main(String[] args){
            List<String> list = new ArrayList<String>();
    
            list.add("111");
            list.add("222");
            list.add("222");
            list.add("333");
            list.add("444");
            list.add("333");
            //foreach循环删除
            for (String str : list){
                if (str.equals("222")){
                    list.remove(str);
                }
            }
    
            System.out.println(Arrays.toString(list.toArray()));
        }

    运行结果:

    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 joe.effective.Test.main(Test.java:20)

    报错。

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

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

    总结:forEach循环删除报错。

  • 相关阅读:
    Validation failed for one or more entities. See 'EntityValidationErrors' property for more details
    Visual Studio断点调试, 无法监视变量, 提示无法计算表达式
    ASP.NET MVC中MaxLength特性设置无效
    项目从.NET 4.5迁移到.NET 4.0遇到的问题
    发布网站时应该把debug设置false
    什么时候用var关键字
    扩展方法略好于帮助方法
    在基类构造器中调用虚方法需谨慎
    ASP.NET MVC中商品模块小样
    ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现
  • 原文地址:https://www.cnblogs.com/Joe-Go/p/10419573.html
Copyright © 2020-2023  润新知