for循环是Iterator的简化操作方式,可以方便地对一个数组或列表进行遍历。
今天遇到一个小tip,就是在for循环中不能对列表进行“结构性变动操作”,什么是结构变动操作呢?简单来说就是remove、add这样对原有列表元素进行增删的操作。
举个例子:
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
a.add("3");
for (String temp : a) {
if ("1".equals(temp)) {
a.remove(temp);
}
}
//上边的for循环等价于下边这个迭代器循环
//Iterator<String> iterator = a.iterator();
//while(iterator.hasNext()){
// String s = iterator.next();
// if("1".equals(s))
// a.remove(s);
//}
这段代码想要实现遍历找到值为“3”的元素,并删除该元素
看似没什么问题,但在运行时会抛出java.util.ConcurrentModificationException异常。
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at com.leif.action.Test.main(Test.java:14)
这是因为在for循环中不支持对列表进行增/删的操作。
我们从头运行调试,看一下源码是怎么处理的:
1.初始化
这个不用多说了
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
a.add("3");
2.for循环初始化
for (String temp : a)
首次进入for循环会初始化生成迭代器(没错,for循环其实是迭代器遍历的简化操作),源码如下:
//java.util.AbstractList
public Iterator<E> iterator() {
return new Itr();
}
这里的Itr()是ArrayList父类AbstractList的一个内部类,用来实现遍历操作。
//java.util.AbstractList.Itr
private class Itr implements Iterator<E> {
int cursor; // 下一个遍历到的元素的下标
int lastRet = -1; // 上一个遍历到的元素的下标
int expectedModCount = modCount;
这里是初始化执行的命令。
cursor: 下一个遍历到的元素的下标,目前还没有开始遍历,所以cursor是0
lastRet:上一次操作的元素的下标,初始值为-1
expectedModCount 和 modCount:这两个就是对队列有结构性操作的计数器,具体作用我们下边看到,对列表有remove或者add操作都会在这两个计数器上体现出来。
比如现在modCount的值就是3,因为我们在初始化列表的时候进行了三次add操作
3.for循环遍历
初始化完迭代器后,开始迭代器的遍历操作:
//java.util.AbstractList.Itr.hasNext()
public boolean hasNext() {
return cursor != size;//0!=3
}
注意,这里执行的hasNext是ArrayList的内部类Itr的hasNext()方法。
cursor: 下一个遍历到的元素的下标
size:列表的大小,这里是3
cursor!=size说明还没遍历到列表的结尾,就会调用next()方法来获取元素
//java.util.AbstractList.Itr.next()
public E next() {
checkForComodification();//检查是否有进行增删操作,见下方源码
int i = cursor;
if (i >= size)//cursor越界
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;//获取列表数组
if (i >= elementData.length)//如果将要遍历的下标大于等于列表的大小,则说明之前对列表有过增删操作,否则i也就是cursor是肯定不会大于等于length的
throw new ConcurrentModificationException();
cursor = i + 1;//cursor+1,移向下一元素
return (E) elementData[lastRet = i];//返回当前元素,并把lastRet指向当前元素
}
//java.util.AbstractList.Itr.checkForComodification()
final void checkForComodification() {
if (modCount != expectedModCount)//如果两者不相同,则说明有对列表进行过增删操作
throw new ConcurrentModificationException();
}
4.删除元素
temp会指向返回对象的地址,这时候进行删除操作,调用的是ArrayList.remove()方法,我们来看看怎么运行的。
//main
for (String temp : a) {
if ("1".equals(temp)) {
a.remove(temp);
}
}
//java.util.ArrayList.remove(Object o)
public boolean remove(Object o) {
if (o == null) {//是否要删除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);//删除操作,见下,此时index为0
return true;
}
}
return false;
}
//java.util.ArrayList.fastRemove(int index)
private void fastRemove(int index) {
modCount++;//进行了结构性变化的删除操作,modCount自增,注意,此时modCount就会和expectedModCount不同,下次遍历的时候就会抛出异常
int numMoved = size - index - 1;//numMoved就是删除后要移动的元素,size=3,index=0
if (numMoved > 0)
System.arraycopy(elementData, index+1,elementData, index,numMoved);
elementData[--size] = null; // clear to let GC do its work
}
5.继续遍历
此时删除了那个元素,继续遍历
//java.util.AbstractList.Itr.hasNext()
public boolean hasNext() {
return cursor != size;//1!=2
}
cursor!=size说明还没遍历到列表的结尾,就会调用next()方法来获取元素
//java.util.AbstractList.Itr.next()
public E next() {
checkForComodification();//异常会在此抛出,见下
int i = cursor;
if (i >= size)//cursor越界
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;//获取列表数组
if (i >= elementData.length)//如果将要遍历的下标大于等于列表的大小,则说明之前对列表有过增删操作,否则i也就是cursor是肯定不会大于等于length的
throw new ConcurrentModificationException();
cursor = i + 1;//cursor+1,移向下一元素
return (E) elementData[lastRet = i];//返回当前元素,并把lastRet指向当前元素
}
//java.util.AbstractList.Itr.checkForComodification()
final void checkForComodification() {
if (modCount != expectedModCount)//4!=3 抛出异常!!
throw new ConcurrentModificationException();
}
至此,我们已经看完了从初始化到抛出异常的整个流程,可见该异常是ArrayList开发者有意而为之的,目的就是不想让使用者在遍历的时候进行删除(或添加)操作。
原因我认为可能就是因为这样操作是非线程安全的吧,如果多个线程同时对同一个ArrayList进行遍历,其中一个进行了增删操作的话,那么另外一个线程在遍历的时候就会出问题。