背景

前一段时间被问到了关于 List 集合的安全删除元素问题。一时间没反应过来这问题问的是什么,安全体现在什么地方,线程安全?线程安全可以保证元素粒度的数据唯一吗?删除是指什么,list.remove()?
带着这些疑问,重温了一下Java的集合知识。

问题分析

List为什么需要安全移除?

我不理解什么是安全删除,我开发的业务中也很少说需要用到remove的,我只记得一般用的话,都是remove(index)这样。写个测试代码看看

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (int i = 0; i < list.size(); i++) {
    if ("B".equals(list.get(i))) {
        list.remove(i);
        continue;
    }
    System.out.println(list.get(i));
}

这段代码的目的就是想把B移除,最后期望的输出只有AC两个字母,看一下运行结果:

清空实体类的值Java java清空list_list


目的达到了,那这个删除不就是安全删除吗?怎么才算安全删除?

于是我又加了一堆 A B C D E F G,还是删除B,最后打印的数组也是正确的。

直到我突发奇想,多加了一个连续重复的字母B,问题出现了

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("B");
list.add("C");
list.add("D");

for (int i = 0; i < list.size(); i++) {
    if ("B".equals(list.get(i))) {
        list.remove(i);
        continue;
    }
//            System.out.println(list.get(i));
}
System.out.println(list);

我打印的结果是

[A, B, C, D]

为什么会这样呢?其实原理很简单,就是因为List.remove删除元素后,数组的整体下标会往前移动,原本的位置被遍历过了,就会被跳过。

A

B

B

C

D

遍历index

0

1

2

3

4

0遍历元素A,不作任何操作

0

1

2

3

4

1遍历第一个B,移除B

0

1

2

3

2遍历C,已经跳过第二个B

0

1

2

3

3遍历D,后面没有元素了,结束

那简单啊,我记得list移除操作可以remove(object),试一下

for (String element : list) {
    if (element.equals("B")) {
        // 在for循环中直接使用list.remove()方法删除元素
        list.remove(element);
    }
}
System.out.println(list);

直接给我报错了

清空实体类的值Java java清空list_安全_02


分析了一下ArrayList的源码,原来增强for循环的实现原理是使用了Iterator迭代器,而ArrayList重写了迭代器的next方法,每次迭代时会检查是否做了新增或者删除操作(modCount++),而这些操作都会导致期待值与实际值不对等,从而抛出异常。

说简单点就是和两个B字母无关,是你使用了增强for循环就不可以在遍历的时候add和remove。

和上面相同的代码原理是这样的,使用迭代器遍历list,随后用ArrayList的remove

Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            if (element.equals("B")) {
                list.remove(element); // 直接使用list.remove()方法删除元素
            }
        }

试了一下,也是报错,一模一样的问题。

到现在为止,我们理解了怎样场景下移除元素是不安全的。不安全包括:

  1. 下标上移导致的检查丢失
  2. ConcurrentModificationException的发生

问题解决

方案一:

查阅了Java的API文档之后,上面提到,使用Iterator自己的remove方法可以安全地移除元素。

List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("B");
        list.add("C");
        list.add("D");
        
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            if (element.equals("B")) {
                iterator.remove(); // 安全移除元素
            }
        }

        System.out.println(list);

输出的结果为

[A, C, D]

方案二

Java8之后list新增了一个api removeIf,这个也可以做安全删除

list.removeIf(s -> s.equals("B"));

输出的结果为

[A, C, D]

方案三

使用removeAll方法

List<String> elementsToRemove = new ArrayList<>();
        for (String element : list) {
            if (element.equals("B")) {
                elementsToRemove.add(element);
            }
        }

        list.removeAll(elementsToRemove);

这样执行的结果也是正确的