先看需求,现有一个ArrayList,泛型是String,且内含有四个元素"a","b","b","c"。
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
现需要删除集合中元素为"b"的所有元素。
先来看第一种错误写法:
String s ;
for (int i = 0; i <list.size() ; i++) {
s = list.get(i);
if("b".equals(s)){
list.remove(s);
}
}
System.out.println(list.toString());
运行结果:
结果只删除了第一个b,第二个还好好的待在那里!
WTF??这么简单的逻辑竟然会出现问题?别着急我们打开源码看看,究竟是为什么。直接打开list.remove方法。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
根据我们的情况他应该是直接进到了else里执行循环进行匹配,在匹配结果为true后进入fastRemove方法。我们继续深入。
首先,modCount++,这个属性记录着List的结构修改次数(增删,改不算)。第二句记录着删除一个元素后,该index的后面的元素共需要挪几位。这里我们要删除第一个"b"元素 索引是 1 ,numMoved = 2,所以是大于0的。然后问题就出现在下一步操作。它执行了System.arraycopy方法。这个方法意思是从源数组的index+1元素开始复制后面的所有元素到目标数组的index位置开始,numMoved个长度。
在我们的场景来看就是这样的:
然后最后一步,移除末尾元素,末尾的"c"被删除了。
聪明的小伙伴已经发现问题了,移除这一步操作是没有问题的。但是我们的for循环仍在继续,下一次循环i=2,而此时index==2的元素是 "c" !所以equals返回了false,至此执行结束。导致只删除了第一个"b"。
再来看第二种错误写法:
for(String s:list){
if(s.equals("b")){
list.remove(s);
}
}
第二次我们使用增强for循环来演示一把。
运行结果报出了著名的并发修改异常 java.util.ConcurrentModificationException。看到这里有的小伙伴又要懵逼了,我单线程操作集合,怎么还会报出并发修改的异常???
熟悉集合的小伙伴可能知道,增强for循环遍历集合其实是使用了Iterator迭代器的hasNext,next来遍历集合。我们这种写法其实是使用Iterator遍历,但是用了ArrayList的remove方法做删除。下面我们来看看这样会出现怎么样的问题。
从控制台报错信息我们也不难看出,正是ArrayList中的内部类Itr的checkForComodification方法。进去看看。
截取ArrayList内部类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];
}
关于这个Itr的源码我会在其他文章再做解读,我们先关注本次的重点。
请看next方法的第一行代码调用了checkForComodification()。这不就是刚才出问题的代码吗,我们进去看看。
modcount记录着List结构修改的次数,在上文中已经说过了。明确了这一点,我们再看,Itr在初始化的时候会将modCount赋给expectedCount。(这里其实是用来检测并发错误的,这涉及到多线程的知识。简言之如果在Itr迭代的过程中List被结构修改,那么它的modCount必定会增加。于是到了Itr获取元素的时候就会发现expectedCount与之前的modCunt不一致从而提醒用户并发异常)。因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。
那么怎样才能优雅的删除掉这两个"b"元素呢?
这里提供两种解决方案:
- 第一种是从后向前遍历ArrayList。因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
for(int i=list.size()-1;i>=0;i--)
{
Strings=list.get(i);
if(s.equals("b"))
{
list.remove(s);
}
}
- 第二种是我比较推荐的,显示的使用Iterator遍历并删除该元素。
Iterator<String> it = list.iterator();
while (it.hasNext())
{
String s = it.next();
if (s.equals("b"))
{
it.remove();
}
}