1. 引出问题

先看这个反面例子:
当我试图在for循环内修改数组元素时,Java虚拟机抛出异常并退出。

//获取一个内部包含10个元素的List,下文默认省略此方法
    public List<String> getList() {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add("元素" + i);
        }
        return list;
    }

    public void failTest() {
        List<String> list = getList();
        for (String element : list) {
            if ("元素3".equals(element) || "元素7".equals(element)) {
                list.remove(element);
            }
        }
        System.out.println(list);
    }

运行结果:

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
(省略)

2. 产生原因

经过查找资料,发现是触发了Java集合框架提供了一种错误检测机制,称为"fail-fast"(快速失败)。它的目标是在并发修改异常的情况下立即检测到集合的结构被修改的情况,并抛出 ConcurrentModificationException异常。

"Fail-fast"机制通过迭代器来实现。当创建一个迭代器时,迭代器会记录原始集合的修改次数。如果在迭代期间,通过集合的其他操作改变了集合的结构(例如添加、删除元素),迭代器会立即抛出 ConcurrentModificationException异常。

这种机制的存在是为了确保多线程环境下集合的一致性和安全性。它提醒开发人员在多线程环境下避免对集合进行并发修改,以防止出现不确定行为。

虽然上文的反例中使用的是单线程,但是在for循环遍历集合或数组时,在循环体内只能读取集合或数组中的元素,而不能对其进行修改(只读)。如果在循环体内尝试对集合或数组进行修改,将会导致编译错误。

3. 关于迭代器

Java迭代器(Iterator)是一种用于遍历集合元素的对象。它是Java集合框架的一部分,提供了一种统一的方式来访问集合中的元素,而不依赖于集合的具体实现。

迭代器提供了以下常用的方法:

  • hasNext():判断集合中是否还有下一个元素。如果有,返回 true;否则,返回 false
  • next():返回集合中的下一个元素,并将迭代器的位置前进到下一个元素。
  • remove():从集合中删除通过迭代器访问的上一个元素。这个方法是可选的,不是所有的集合都支持。

迭代器可以用于遍历任何实现了 Iterable接口的集合,如ArrayList、LinkedList、HashSet等。它提供了一种安全、可靠的遍历集合的方式,而不需要暴露集合的内部结构。此外,迭代器还支持在遍历过程中删除集合中的元素,而不会抛出 ConcurrentModificationException异常。

Java的增强型for循环(也称作for-each)for (String element : list) {} ,底层就是由迭代器来实现的,源码大致如下:

```java
for (Iterator<String> iterator = collection.iterator(); iterator.hasNext(); ) {
    String element = iterator.next();
    // 对元素进行操作
}

4. 问题的解决方法

(1)使用迭代器

既然介绍迭代器时提到了它是“一种安全、可靠的遍历集合的方式”,那么它当然可以用来解决本文的问题:

    public void iteratorTest() {
        List<String> list = getList();
        Iterator<String> iterator = list.listIterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            if (element.equals("元素3") || element.equals("元素7")) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }

(2)使用CopyOnWriteArrayList

CopyOnWriteArrayList是Java提供的一个线程安全的List实现类。它是在Java 5中引入的,并位于java.util.concurrent包中。

CopyOnWriteArrayList是一个可变的数组列表,在对其进行修改时会创建一个新的数组,并将新的元素添加到新的数组中。这意味着读取操作(如get和size)可以同时进行,而不需要进行加锁操作,因此读取操作具有较低的开销。但是在进行写入操作时,由于每次写操作都会创建一个新的数组,所以写操作的性能较低,并且会阻塞其他读写操作,尤其是在列表很大的情况下。

总结:适用于读操作频繁、写操作较少的场景;写入时会创建新数组,需要额外的内存开销。

    public void cow() {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(getList());
        for (String item : list) {
            if ("元素5".equals(item)) {
                list.remove(item);
            }
        }
        System.out.println(list);
    }

最后:

CopyOnWriteArrayList是线程安全的,多线程不需要加锁。迭代器 Iterator在多线程要注意加锁。


标题:在Java循环里修改元素的正确操作
作者:xiaojie
地址:https://xiaojie.dev/articles/2020/04/10/1692005434370.html