1.原始的for循环删除集合元素的时候有可能会漏掉某个元素
测试代码:
/**
* 假设需要删除集合中的所有的"A"元素
* 如果使用原始的for循环,那么就可能会漏删
*/
List<String> list = new ArrayList<>();
list.add("A");list.add("A");list.add("B");
for(int i=0; i<list.size(); i++) {
if("A".equals(list.get(i))) {list.remove(i);
}
}
System.out.println(list);
输出结果:
[A, B]
可以看到集合中只有一个"A"被删了,第二个"A"却还在。
其实解决办法也很简单,在删除元素之后再加一行代码i--
就解决了
2.增强型for循环删除集合元素会报出ConcurrentModificationException异常
修改一下上面的测试代码
List<String> list = new ArrayList<>();
list.add("A");
list.add("A");
list.add("B");
for(String item : list) {
if("A".equals(item)) {
list.remove(item);
}
}
System.out.println(list);
打印结果是ConcurrentModificationException异常。
增强型for循环其实是编译器认可的语法,在到了虚拟机那边只认迭代器循环,所以在编译为.class文件的时候,最终还是变成了迭代器循环。通过反编译得到如下代码:
List<String> list = new ArrayList();
list.add("A");
list.add("A");
list.add("B");
Iterator var3 = list.iterator();
while(var3.hasNext()) {
String item = (String)var3.next();
if ("A".equals(item)) {
list.remove(item);
}
}
迭代器这块比较复杂,我们从源码上慢慢探索。
list.iterator()其实际上是获得了一个内部类Itr对象,这个类实现了Iterator接口。循环所用到的hasnext(),next()方法都是在这个类中实现的。
以下代码来自openJDK1.8的AbstractList
类中
以下代码来自ArrayList
大概理一下抛出异常的思路:
迭代器循环每次调用next方法,next方法中第一步会先调用checkForComodification
方法,checkForComodification方法中会校验modCount
和expectedModCount
两个变量是否一样(我把这两个变量理解为版本号),如果不一样就会抛出ConcurrentModificationException
异常。
modCount是集合的成员变量,初始值为1,当创建迭代器对象的时候,modCount会赋值给expectedModCount。当调用了list本身的remove方法的时候,会让expectedModCount自增,却不改变modCount。于是到了下一次迭代器的next方法去取集合元素的时候会先执行checkForComodification()方法判断版本号是否一致,这时候集合的版本号和迭代器的版本号不一致就抛出了异常。
而如果是调用迭代器的remove方法,它会先调用集合的remove方法,然后执行cursor- -,再把modCount赋值给expectedModCount。这样一来既保证了下一次迭代的时候不会漏过任何元素,也避免了next方法校对版本号的时候抛出异常
3.在循环中删除集合元素的正确方法
在循环中删除元素的正确方法是用迭代器的remove方法
List<String> list = new ArrayList<>();
list.add("A");
list.add("A");
list.add("B");
for(Iterator<String> i = list.iterator(); i.hasNext(); ) {
String item = i.next();
if ("A".equals(item)) {
//这里调用迭代器的rermove方法而不是集合的remove方法
i.remove();
}
}
System.out.println(list);
也许你会觉得上面的写法太麻烦,“我只是想删除集合中的所有A元素,有没有更优雅的写法呢”。答案是有的,在Java8以后,我们就可以用一行代码来完美代替上面的写法了:
list.removeIf(item -> "A".equals(item));
实际上removeIf方法内部也是用了迭代器循环
4.为什么要设计“快速报错”机制
这是一种集合的保护机制,为了防止多个线程同时修改同一个集合的内容。我们先假设没有快速失败机制,当一条线程正在迭代遍历某个集合的过程中,另一个线程介入其中,并且删除了集合内的某个对象,那么就出问题了:就像上面说过的一样,元素被删除后,集合缩容,第一个线程要进行下一次迭代时,就会漏掉一个元素。这种数据不同步所带来的问题可能再当前步骤下不会出现异常,直到在后续的某个代码环节中才会暴露出问题,从而给我们排查定位异常带来难度。
而有了快速失败机制后,一个线程的删除了集合中的某个元素,集合的modCount
+1,另一个集合调用迭代器的next方法,发现版本号不一致,于是就直接抛出异常,终止迭代,让我们能够快速的定位到问题所发生的时间和位置。
ConcurrentHashMap
、CopyOnWriteArrayList
、CopyOnWriteArraySet
都使用了可以避免ConcurrentModificationException的机制。这个话题我们下次再研究吧。
附上我自己遍历删除List的方法
直接采用正常的for进行遍历,但是使用倒序进行遍历,这样就不会遇到集合下标产生的问题~
public static void main(String[] args) {
ArrayList<String> list = new ArrayList();
list.add("_");
list.add("_");
list.add("+");
list.add("_");
list.add("_");
System.out.println(list);
for (int i = list.size() - 1; i >= 0 ;i --) {
if(list.get(i) == "_"){
list.remove(i);
}
}
System.out.println(list);
}