阿里Java手册剖析-6.14【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,

987 阅读7分钟

阿里Java开发手册剖析:


【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

这里主要包含三层意思:

  1. 不要在 foreach 循环里进行元素的 remove/add 操作。
  2. 使用 Iterator方式 可以remove元素
  3. 高并发下,使用 Iterator方式时,需要加锁。

第3个好理解,直说前两个。

foreach中remove/add操作会报异常

演示demo:


 private static  void test57() {
        List<String> list = new ArrayList<String>();
        list.add("1");
        list.add("2");

        for (String s : list) {
            if ("2".equals(s)) {
                list.remove(s);
            }
        }

        System.out.println(list.toString());

日志输出:

异常意思是有多处在同时修改。那么是不是可以得出结论:

foreach 循环里进行元素的 remove/add 操作,就会报java.util.ConcurrentModificationException

再看下面例子,就知道没这么简单,我们就if("2".equals(s))改成if("1".equals(s))。移除第一个元素:



 private static  void test57() {
        List<String> list = new ArrayList<String>();
        list.add("1");
        list.add("2");

        for (String s : list) {
            if ("1".equals(s)) {
                list.remove(s);
            }
        }

        System.out.println(list.toString());

日志输出:

[2]

Process finished with exit code 0

发现这次并没报异常了。好奇下我改了下demo,list中添加了第三个元素,依旧移除第一元素:



 private static  void test57() {
        List<String> list = new ArrayList<String>();
        list.add("1");
        list.add("2");
        list.add("3");

        for (String s : list) {
            if ("1".equals(s)) {
                list.remove(s);
            }
        }

        System.out.println(list.toString());

此时又报java.util.ConcurrentModificationException异常了。

所以上面问题可以分为两个:

  1. foreach中remove/add为什么会报ConcurrentModificationException异常
  2. foreach中remove倒数第二个元素为什么又不会报ConcurrentModificationException异常

foreach中remove/add为什么会报ConcurrentModificationException异常

我们可以先找到异常的抛出地方,然后再反推他的调用栈。除了直接看异常日志,还可以借助debug工作:

1.在任意断点处右击,然后点击More按钮(或者直接快捷键)

2.添加一个新断点

3.选择为一个java异常断点

  1. 搜索具体异常类型:java.util.ConcurrentModificationException

  1. 确认

6.debug程序:

7.发生异常后,程序会自动断在异常处

  1. 同时可以看到调用栈:

我们看到forech内部遍历调用了Iterator。异常也是在Iterator.next()方法中触发的。 我将程序的字节码打出来,看下foreach到底是怎么执行:


public class rongcheng.collection.arrayList.ConcurrentModification {
  public rongcheng.collection.arrayList.ConcurrentModification();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String 1
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: ldc           #6                  // String 2
      20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      25: pop
      26: aload_1
      27: ldc           #7                  // String 3
      29: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      34: pop
      35: aload_1
      36: invokeinterface #8,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      41: astore_2
      42: aload_2
      43: invokeinterface #9,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
      48: ifeq          81
      51: aload_2
      52: invokeinterface #10,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      57: checkcast     #11                 // class java/lang/String
      60: astore_3
      61: ldc           #4                  // String 1
      63: aload_3
      64: invokevirtual #12                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      67: ifeq          78
      70: aload_1
      71: aload_3
      72: invokeinterface #13,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z
      77: pop
      78: goto          42
      81: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream;
      84: aload_1
      85: invokevirtual #15                 // Method java/lang/Object.toString:()Ljava/lang/String;
      88: invokevirtual #16                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      91: return
}

看到第36行,当foreach开始的时候,通过List.iterator()的方法获得了一个Iterator对象,后面又有Iterator.hasNext()Iterator.next()。结合异常的触发在Iterator.next()。我大胆地猜测for (String s : list) {}的本质就是:

  		
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
           ...
        }

我将上面foreach代码改成Iterator实现,并输出字节码返现确实一样:

 public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("1");
        list.add("2");
        list.add("3");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            if (item.equals("1")) {
                iterator.remove();
            }
        }

        System.out.println(list.toString());
    }

字节码:

 public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String 1
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: ldc           #6                  // String 2
      20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      25: pop
      26: aload_1
      27: ldc           #7                  // String 3
      29: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      34: pop
      35: aload_1
      36: invokeinterface #8,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      41: astore_2
      42: aload_2
      43: invokeinterface #9,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
      48: ifeq          79
      51: aload_2
      52: invokeinterface #10,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      57: checkcast     #11                 // class java/lang/String
      60: astore_3
      61: aload_3
      62: ldc           #4                  // String 1
      64: invokevirtual #12                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      67: ifeq          76
      70: aload_2
      71: invokeinterface #13,  1           // InterfaceMethod java/util/Iterator.remove:()V
      76: goto          42
      79: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream;
      82: aload_1
      83: invokevirtual #15                 // Method java/lang/Object.toString:()Ljava/lang/String;
      86: invokevirtual #16                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      89: return

查看ArrayList源码看到内部确实有iterator()方法,这验证了上面猜测了。


public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

 /**
     * Returns an iterator over the elements in this list in proper sequence.
     *
     * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
     *
     * @return an iterator over the elements in this list in proper sequence
     */
    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;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        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();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

总结:ArrayList内部有个modCount变量,用来记录整个List的结构被修改的次数(比如:ArrayList.remove(),ArrayList.add()都会造成modeCount++), Itr内部有个expectedModCount:预期的改变次数,每次Iterator.next()会先检测modCount==expectedModCount,如果两者不同则>会报java.util.ConcurrentModificationException异常。modCount!=expectedModCount的原因是,list.remove导致modeCount++了。

同时上面源码也解释了为什么使用Iterator遍历元素时,remove元素后不会导致ConcurrentModificationException


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();
            }
        }

我们可以看到Iterator的remove()的本质还是调用了ArrayList.this.remove()。 第一次 checkForComodification();时,两个count都没改变。而执行ArrayList.this.remove()后她立即将expectedModCount = modCount进行同步,所以后续执行checkForComodification()方法不会再抛异常。

不过此处并非原子操作,所以高并发时是可能会报异常。所以前面第三条建议是:高并发情况下,需要对 Iterator 对象加锁

foreach中remove倒数第二个元素为什么又不会报ConcurrentModificationException异常


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
    }


可以看到不管哪种形式的remove都会导致ArrayList.this的--size。而这步结束后,会进行下一轮循环。此时会调用Iterator.hasNext()方法:

 private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

}

此时就导致return false。因此本次遍历就会结束从而导致不会继续后面逻辑(Iterator.next()),也就无法触发ConcurrentModificationException 下面demo可以验证上述说法:


public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("1");
        list.add("2");
        list.add("3");

        for (String s : list) {
            System.out.println(s);
            if ("2".equals(s)) {
                list.remove(s);
            }

        }
    }

日志输出:

1
2

Process finished with exit code 0

removeIf

java 8 提供了更简单安全的方式list.removeIf(item -> item.equals("1"));