List循环删除元素最佳实践及错误案例分析

在平常开发中,我们会经常碰到需要从一个list集合中,筛选过滤出符合条件的元素。或者说循环遍历list,删除掉不符合条件的元素。

举个例子:现有一个list,需要删除所有“李”姓的人名。

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));

小贴士:这里为什么不直接用Arrays.asList返回的值而初始化了一个新的ArrayList?
具体原因我们可以看一下Arrays.asList的源码

public static <T> List<T> asList(T... a) {
    return new Arrays.ArrayList<>(a);
}

/**
 * @serial include
 */
private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable{}

可以看出,虽然Arrays.asList返回的类也叫ArrayList,但是这是Arrays自己定义的ArrayList,他并没有实现remove方法,故在删除元素是会报异常。

public E remove(int index) {
    throw new UnsupportedOperationException();
}



书归正传,回到样例,我们先看看最佳实现的写法,也就是推荐的写法:

1、使用Iterator

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String next = iterator.next();
    if (next.startsWith("李")) {
        iterator.remove();
    }
}
System.out.println(list);

2、使用removeIf

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
list.removeIf(next -> next.startsWith("李"));
System.out.println(list);

使用了Lambda的写法,通过查看removeIf源码即可发现,实际原理其实与1一致。

default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}

3、stream流+filter

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
list = list.stream().filter(e -> !e.startsWith("李")).collect(Collectors.toList());
System.out.println(list);

与1、2的区别为,方法3相当于是从原始list中挑选出符合条件的元素添加到一个新的list,也就是说过滤前和过滤后的list对象不是同一个,结果值新建了一个list对象。



不推荐的写法,或者说这种写法可能还会出现错误

4、for循环删除(结果错误)

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    if (s.startsWith("李")) {
        list.remove(s);
    }
}
System.out.println(list);

这种写法可以发现,结果是错误的,原因是在for循环中判断结束的条件为list.size(),而随着删除元素,这个值是变化的,也就是会提前结束循环,并不能遍历到全部元素。
我们可以通过增加一句i—来解决这个问题:

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    if (s.startsWith("李")) {
        list.remove(s);
        i--;
    }
}
System.out.println(list);

或者使用倒序遍历:

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
for (int i = list.size() - 1; i >= 0; i--) {
    String s = list.get(i);
    if (s.startsWith("李")) {
        list.remove(s);
    }
}
System.out.println(list);

5、增强for循环删除(抛异常)

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
for (String s : list) {
    if (s.startsWith("李")) {
        list.remove(s);
    }
}
System.out.println(list);

会抛出java.util.ConcurrentModificationException异常

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)

从异常可以看出,这种方式使用的还是迭代器循环,只是在删除时使用的是list的remove方法而不是迭代器的remove方法,相当于:

List<String> list = new ArrayList<>(Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String next = iterator.next();
    if (next.startsWith("李")) {
        list.remove(next);
    }
}
System.out.println(list);

而异常的报错点就在于iterator.next();方法中,该异常为集合操作中很常见的异常之一,即并发修改异常。
迭代器iterator在取下个元素的时候都会去判断要修改的数量(modCount)和期待修改的数量(expectedModCount)是否一致,不一致则会报错,而 ArrayList 中的 remove 方法并没有同步期待修改的数量(expectedModCount)值,所以会抛异常了。


觉得内容还不错?打赏个钢镚鼓励鼓励!!👍