ArrayList遍历时删除元素
ArrayList作为集合,一般有三种遍历方式,分别是普通for遍历,增强for遍历(foreach遍历)和迭代器遍历,采用不同的遍历方式时,有的方式可以删除元素,有的则不可以,首先结论先行:
1.for循环,可以删除元素
2.foreach循环,不可以删除元素
3.迭代器循环删除,调用iterator.remove()可以,使用list.remove()不可以
接下来分析不同方式的出现不同结果的原因:
1.使用foreach遍历
先贴代码样例:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
for(String str:list){
System.out.println(str);
if(str.equals("aa")){
list.remove("aa");
}
}
System.out.println(list);
}
执行结果为:
这里有有一个很有意思的现象,就是遍历删除倒数第二个元素的时候,是可以成功删除的,并不会报错,删除其余位置的元素都会报错,代码和结果如下:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
for(String str:list){
System.out.println(str);
if(str.equals("cc")){
list.remove("cc");
}
}
System.out.println(list);
}
是不是感觉很神奇,这个出现的原因后面分析
2.迭代器遍历
先贴代码样例:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String str = iterator.next();
System.out.println(str);
if("aa".equals(str)){
iterator.remove();
}
}
System.out.println(list);
}
执行结果为
这里有一个注意的点是必须使用迭代器的remove()方法,不能使用list.remove()方法,否则会抛出错误,原因同样后面分析,看一下代码样例和执行结果
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String str = iterator.next();
System.out.println(str);
if("aa".equals(str)){
list.remove("aa");
}
}
System.out.println(list);
}
3.普通for查询遍历
老规矩,先来代码样例
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
for(int i =0;i<list.size();i++){
System.out.println(list.get(i));
if("dd".equals(list.get(i))){
list.remove("dd");
}
}
System.out.println(list);
}
执行结果为
4.分析原因
首先,先看一下ArrayList的继承关系图
首先继承了AbstractList类,在这个类中有一个全局变量modCount,这个变量是用来记录当list中元素变化时(新增和删除)的操作次数,当Arraylist使用foreach遍历时,会根据集合对象创建一个iterator迭代对象(在ArrayList中是一个Itr的内部类),用这个迭代对象来遍历集合,而使用迭代器遍历时主要会用到两个方法hasNext()和next()方法,ltr内部类源码如下
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;
// prevent creating a synthetic constructor
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
...
}
当迭代器遍历时,会首先调用hasNext方法,判断是否有下一个,如果为true则会调用next()方法,使用next方法首先会校验modCount和expectedModCount是否相等,如果不相等就会抛出上面执行结果的中的异常,那么为什么不相等呢,我们看一下ArrayList的remove(Obejct o)方法
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
//调用快速删除方法
fastRemove(es, i);
return true;
}
在这个方法中会调用fastRemove()方法,源码如下
private void fastRemove(Object[] es, int i) {
//每调用一次mmodCount就会自增一次
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
我们发现每调用一次remove()方法,modCount就会自增,而expectedModCount 默认和modCount相等,当modCount自增加1,而expectedModCount 没有增加,就会抛出异常,这也就是foreach遍历时抛出的错误的原因,使用迭代器遍历,使用list.remove()抛出错误也是同样的原因,那么为什么使用foreach删除第二个元素时就能成功呢,这是因为hasNext()方法返回的结果为false,迭代器认为没有下一个元素了,就不会执行next(),就不会进行modCount和expectedModCount 的判断,上面的执行结果也能看到,当删除倒数第二个元素时,最后一个元素并没有遍历打印,这又是为什么呢?因为hasNext()方法中cursor变量的值和此时的size值相等。
cursor类似于一个游标或者指针,每进行一次遍历时就会+1,开始默认为0,以上面的代码实例,遍历第一次时,cursor=0,size=4,末尾cursor+1变成1;第二次时cursor=1,size=4,末尾cursor+1变成2;第三次时cursor=2,size=4,末尾cursor+1变成3,此时因为调用了list.remove()所以此时size-1变成了3;第四次时cursor=3,size=3,cursor=size,便不会获取最后一个元素,也就不会校验和抛出异常。
那么又为什么迭代器遍历时调用iterator.remove()不会报错呢?这是因为在iterator.remove()时会同步修改expectedModCount的值,使其和modCount的值一致,就会通过校验,不会抛出错误
使用普通for循环时,因为没有expectedModCount和modCount的比较,也就不会抛出异常
结尾
所以结论就是开始的结论,需要注意的事,前面提到过新增也会使modCount变化,而迭代器每一次遍历都会校验modCount和expectedModCount 是否一致,所以使用foreach遍历使,是不能够调用list.add()新增的,也会抛出同样的错误,这里贴一下样例和实验结果,感兴趣的朋友可以尝试一下,以上观点属于个人见解,如有问题,欢迎讨论
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
for(String str:list){
System.out.println(str);
list.add("ee");
}
System.out.println(list);
}