Skip to main content

Command Palette

Search for a command to run...

foreach 中无法删除和添加元素

Updated
3 min read
foreach 中无法删除和添加元素

fail-fast 是一种通用的系统设计思想,一旦检测到可能会发生错误,就立马抛出异常

throw new ConcurrentModificationException();

抛出 ConcurrentModificationException 异常之后不需要捕获,直接中止程序

示例

class TEST {
    public static void main(String[] args) {
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
        for (Integer i : integers) {
            if (i == 2) {
                integers.remove(i);
            }
        }
    }
}
/*
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 com.TEST.main(TEST.java:143)
*/

ArrayList 中抛出异常的位置:

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

迭代器 Iterator

for-each 本质上是个语法糖,底层是通过迭代器 Iterator 配合 while 循环实现的 反编译后字节码:

ArrayList<Integer> integers = new ArrayList(Arrays.asList(1, 2, 3, 4));
Iterator var2 = integers.iterator();
while(var2.hasNext()) {
    Integer i = (Integer)var2.next();
    if (i == 2) {
        integers.remove(i);
    }
}

ArrayList 中的 Iterator 代码:

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();
        }
    }
  ....
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
  • modCount 用来记录数组被修改次数,每次对数组进行修改的时候都会自增 1

  • 创建 Iterator 的时候 expectedModCount = modCount ,如果在遍历的过程中增加或删除了其中一个元素,那这两个字段就不相等了,在迭代器执行 next 方法之后执行 checkForComodification 检查,抛出 ConcurrentModificationException 异常

  • ArrayList 中的 add 和 remove 方法中有 modcount++ 的代码

  • 这种机制可以保证迭代器在遍历 ArrayList 时,不会遗漏或重复元素,同时也可以在多线程环境下检测到并发修改问题

如何正确删除

Remove 后 break

class TEST {
    public static void main(String[] args) {
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
        for (Integer i : integers) {
            if (i == 2) {
                integers.remove(i);
                break;
            }
        }
        System.out.println(integers);
    }
}
  • 调用 remove 之后直接 break 掉,不会调用 next 函数,也就不会调用检查

  • 无法删除多个重复元素了

for 循环

class TEST {
    public static void main(String[] args) {
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
        for (int i = 0; i < integers.size(); i++) {
            if (integers.get(i)== 2){
                integers.remove(i);
            }
        }
        System.out.println(integers);
    }
}
  • for 循环虽然可以避开 fail-fast 保护机制,但有问题

  • remove 函数会将被删除元素的后面的所有元素前移一位,这样就下标就跳过了一位没有检查

iterator 方式(推荐)

class TEST {  
    public static void main(String[] args) {  
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));  
        Iterator<Integer> iterator = integers.iterator();  
        while(iterator.hasNext()){  
            if (iterator.next()==2){  
                iterator.remove();  
            }  
        }  
    }  
}
  • 使用 iterator 可以 remove 的原因是 ArrayList 的迭代器的 remove 方法中对 expectedModCount = modCount 进行过了修改

Stream 方式(推荐)

Java 8 后可使用

class TEST {  
    public static void main(String[] args) {  
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));  
        integers = integers.stream()  
                .filter(i -> i != 2)  
                .collect(Collectors.toCollection(ArrayList::new));  
        System.out.println(integers);  
    }  
}

removeIf 函数 (推荐)

Java 8 后可使用

class TEST {  
    public static void main(String[] args) {  
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));  
        integers.removeIf(i->i==2);  
        System.out.println(integers);  
    }  
}
  • removeIf 函数是 Java8 后在 Collection 接口中添加的 default 方法,也是用迭代器 Iterator 实现的

  • removeIf 接口中传入的是一个函数式接口,可以用 lamba 表达式和方法引用

  • ArraysList 中的 removeIf 函数重写了 Collection 接口中的默认方法

More from this blog

根据前、中、后序数组构造二叉树

根据两个遍历数组生成二叉树,主要是固定住一个根节点,然后去另一个数组查找下标,划分数组做左右子树,再递归执行左子树和右子树。 这里主要讨论的是使用切片的过程中如何确定切片的起始点,即切片的区间,利用的是左子树的长度。 前序和中序构造二叉树 105. 从前序与中序遍历序列构造二叉树 递归加切片, python 中可以使用 index 函数直接获取值的下标。 注意:切片是左闭右开区间,最后一个值取不到 切片的下标如何思考:利用左子树的长度来辅助思考。idx 是中序数组中的当前节点下标,所以左子...

Apr 3, 20242 min read
根据前、中、后序数组构造二叉树

二叉树的遍历

掌握两种方法进行二叉树的遍历,这里重点看迭代法是怎么写,迭代法使用栈来模拟递归中的栈,也可以使用一种通用方式进行前、中、后序遍历。 递归法 def dfs(root) { // 前序遍历 dfs(root.left) // 中序遍历 dfs(root.right) // 后序遍历 } 迭代法:迭代法是用 stack 栈来模拟递归栈 下面这种写法可以统一前序、中序、后序遍历方式的写法,只需要改变入栈顺序 前序遍历:中,左,右中序遍历:左,中,右后序遍历:左...

Apr 3, 20242 min read
二叉树的遍历

函数式编程在 Java 和 Go 中的应用

函数式编程是一种 "编程范式"(programming paradigm),就是如何编写程序的方法论。 函数式编程特点: 函数是"第一等公民" 只用"表达式",不用"语句" "表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。 没有"副作用" 所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就...

Jun 26, 20237 min read
函数式编程在 Java 和 Go 中的应用

Untitled Publication

13 posts