foreach循环中不能使用remove删除元素的原理解析

Posted ABin-阿斌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了foreach循环中不能使用remove删除元素的原理解析相关的知识,希望对你有一定的参考价值。

声明:

  • 原作者:掘金:https://juejin.cn/user/3940246036953293
  • 原文链接:https://juejin.cn/post/6950438574164541454

前言

  • 相信大家肯定都看过阿里巴巴开发手册,而在阿里巴巴开发手册中明确的指出,不要再 foreach 循环里面进行元素的 add 和remove,如果你非要进行 remove 元素,那么请使用 Iterator 方式,如果存在并发,那么你一定要选择加锁。

foreach

    public static void main(String[] args) 
        List<String> list = new ArrayList<String>();
        list.add("11");
        list.add("22");
        list.add("33");
        list.add("44");

        for (String s : list) 
            if ("22".equalsIgnoreCase(s)) 
                list.remove(s);
            
        
        System.out.println(JSONObject.toJSONString(list));
    

输出结果:

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)
	at org.example.list.Test01.main(Test01.java:22)

Process finished with exit code 1

分析异常:

final void checkForComodification() 
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();


  • 比较两个值 modCountexpectedModCount,那么这两个变量是什么呢?

  • 其中modCount表示集合的修改次数,这其中包括了调用集合本身的add方法等修改方法时进行的修改和调用集合迭代器的修改方法进行的修改。而expectedModCount则是表示迭代器对集合进行修改的次数。

先来看看反编译之后的代码,如下:

    public static void main(String[] args) 
        List<String> list = new ArrayList();
        list.add("11");
        list.add("22");
        list.add("33");
        list.add("44");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) 
            String s = (String)var2.next();
            if ("22".equalsIgnoreCase(s)) 
                list.remove(s);
            
        

        System.out.println(JSONObject.toJSONString(list));
    

  • 看里面使用的也是迭代器,也就是说,其实 foreach 每次循环都调用了一次iteratornext()方法, foreach方式中调用的remove方法,是ArrayList内部的remove方法,会更新modCount属性

我们可以看看ArrayList类中的remove方法

    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;
    

  • 看到此方法中,有一个modCount++的操作,也就是说,modCount会一直更新变化。

  • 我们第一次迭代的时候 11 != 22 ,直接迭代第二次,这时候就相等了,执行remove()方法,这时候就是modCount++,再次调用next()的时候,modCount = expectedModCount 这个就不成立了,所以异常信息出现了,其实也可以理解为在 hasNext() 里面,cursor != size 而这时候就会出现错误了。

  • 也就是说 remove方法它只修改了modCount,并没有对expectedModCount做任何操作。

迭代器

为什么阿里巴巴的规范手册会这样子定义?

它为什么推荐我们使用 Iterator呢?

  • 直接使用迭代器会修改expectedModCount,而我们使用foreach的时候,remove方法它只修改了modCount,并没有对expectedModCount做任何操作,而Iterator就不会这个样子。
   public static void main(String[] args) 
        List<String> list = new ArrayList<String>();
        list.add("11");
        list.add("22");
        list.add("33");
        list.add("44");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext())
            String item = iterator.next();
            if("22".equals(item))
                iterator.remove();
            
        
        System.out.println(JSONObject.toJSONString(list));
    

输出结果:

["11","33","44"]

Process finished with exit code 0

可以看出结果是正确的,下面我们来分析一下:

先来看看反编译之后的代码:

    public static void main(String[] args) 
        List<String> list = new ArrayList();
        list.add("11");
        list.add("22");
        list.add("33");
        list.add("44");
        Iterator iterator = list.iterator();

        while(iterator.hasNext()) 
            String item = (String)iterator.next();
            if ("22".equals(item)) 
                iterator.remove();
            
        

        System.out.println(JSONObject.toJSONString(list));
    

主要观察remove()方法的实现,那么需要先看 ArrayList.class:

    public Iterator<E> iterator() 
        return new Itr();
    

    /**
     * An optimized version of AbstractList.Itr
     */
    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;
        

        public void remove() 
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();     //第一步

            try 
                ArrayList.this.remove(lastRet);   //第二步:调用list的remove方法
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount; 		//第三步:modCount是remove方法去维护更新,
                                                    //由于第一步中校验 modCount 和 expectedModCount 是否相当等
             catch (IndexOutOfBoundsException ex) 
                throw new ConcurrentModificationException();
            
        

        final void checkForComodification() 
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        
    

  1. 调用 checkForComodification()方法,作用:判断modCountexpectedModCount 是否相当;

  2. foreach 方式中调用的remove方法,是ArrayList内部的remove方法,会更新modCount属性;

  3. 将更新后的modCount重新赋值给expectedModCount变量。

Java8的新特性

    public static void main(String[] args) 
        List<String> list = new ArrayList<String>();
        list.add("11");
        list.add("22");
        list.add("33");
        list.add("44");

        list.removeIf("22"::equals);
        System.out.println(JSONObject.toJSONString(list));
    

总结

  • for-each循环不仅适用于遍历集合和数组,而且能让你遍历任何实现Iterator接口的对象;
  • 最最关键的是它还没有性能损失。而对数组或集合进行修改(添加删除操作),就要用迭代器循环。
  • 所以循环遍历所有数据的时候,能用它的时候还是选择它吧。

以上是关于foreach循环中不能使用remove删除元素的原理解析的主要内容,如果未能解决你的问题,请参考以下文章

foreach与for的区别

不要在 foreach 循环里进行元素的 remove / add 操作

foreach和for的区别

Java ArrayList在foreach中remove的问题分析

不要在foreach循环里进行元素的remove/add操作

Java foreach remove问题分析