背景
前一段时间被问到了关于 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两个字母,看一下运行结果:
目的达到了,那这个删除不就是安全删除吗?怎么才算安全删除?
于是我又加了一堆 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);
直接给我报错了
分析了一下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()方法删除元素
}
}
试了一下,也是报错,一模一样的问题。
到现在为止,我们理解了怎样场景下移除元素是不安全的。不安全包括:
- 下标上移导致的检查丢失
- 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);
这样执行的结果也是正确的