遍历集合然后删除符合条件的元素,在刚接触集合的时候应该就会碰到这个问题,最初只看到网上各种博客告诉你推荐使用Iterator来遍历集合,而不应该用for循环,今天我终于从源码的角度彻底理解了为什么用for循环会出错。
普通for循环
下面是使用普通for循环删除元素的例子:
public static void main(String[] args) {
List<Integer> numList = Stream.of(1,1,2,2,3,3,4,4,5,5).collect(Collectors.toList());
for (int i = 0; i < numList.size(); i++) {
if (numList.get(i)<=5){
numList.remove(i);
}
}
numList.forEach(System.out::println);
}
// 正确结果: []
// 实际结果:[1,2,3,4,5]
为什么会出现这个结果呢,下面从源码来分析一下
ArrayList的remove 源码
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
我们每次在调用remove删除元素后,都会将被删除元素之后的所有元素往前移动一位,也就是说我们下一个(i+1) 要遍历的元素被一定到了我们现在(i) 遍历到的位置,但是我们下一次会从i+1的位置开始遍历,导致漏掉一个元素,而且每次remove都会漏掉一个元素,这就解释了为什么会出现上面那个结果了。
解决办法也很简单:每次remove之后都手动 i-- 一下,让下一次遍历还是从i的位置开始就可以得到正确结果
public static void main(String[] args) {
List<Integer> numList = Stream.of(1,1,2,2,3,3,4,4,5,5).collect(Collectors.toList());
for (int i = 0; i < numList.size(); i++) {
if (numList.get(i)<=5){
numList.remove(i);
i--; // 手动调用i--;
}
}
numList.forEach(System.out::println);
}
// 结果: []
增强for(foreach)用到了Iterator,所以先学习一下Iterator就理解foreach为什么会出错了
Iterator
这是使用Iterator遍历删除元素的例子,这也是比较推荐使用的方式
public static void main(String[] args) {
List<Integer> numList = Stream.of(1,1,2,2,3,3,4,4,5,5).collect(Collectors.toList());
Iterator<Integer> iterator = numList.iterator(); // 得到Itr对象
while(iterator.hasNext()){
Integer item = iterator.next();
if (item<=5){
iterator.remove();
}
}
numList.forEach(System.out::println);
}
// 得到正确结果:[]
Itr是ArrayList的对Iterator的实现类,我们调用ArrayList的iterator方法,拿到的就是Itr的对象
public Iterator<E> iterator() {
return new Itr();
}
Itr有三个属性用来控制遍历
int cursor; // 下一个遍历元素
int lastRet = -1; // 当前元素的指针,初始值为-1,调用next()会更新这个值
// 期待的modCount值
// 在new Itr(); 会将当前List的modCount赋值给expectedModCount
// 当对List进行任何的修改时 modCount 都会增加,所以一旦修改List,就会导致expectedModCount和modCount不相等了
int expectedModCount = modCount;
注意:上面三个属性比较重要,请认真看一下注释
Iterator next()源码
public E next() {
checkForComodification(); // 检查 expectedModCount和modCount是否相等,如果不相等则会抛异常
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1; // 更新 cursor
return (E) elementData[lastRet = i]; // 更新lastRet
}
在next()中逻辑如下:
- 判断cursor(下一个元素的下标是否越界),
- 然后会拿到cursor下标对应的元素,
- 再更新cursor和lastRet(当前元素的下标)
Iterator checkCondition 源码
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
Iterator的remove方法源码
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); // 检查 expectedModCount和modCount是否相等,如果不相等则会抛异常
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
remove中逻辑如下:
- 检查当前expectedModCount和modCount是否相等
- 调用集合的remove方法删除当前元素
- 更新expectedModCount(期待的modCount),cursor(下一个元素的下标),lastRet(当前元素的下标)
为什么最后需要更新这三个属性
上面我们提到对List进行任何的修改时 modCount 都会增加,而modCount 增加就会导致expectedModCount和modCount不相等了,所以在Iterator的remove方法中对expectedModCount进行了重新赋值,使得expectedModCount等于新的modCount,这样在下一次调用next() 检查 expectedModCount和modCount是否相等时就不会报错了
增强 for 循环
使用增强for遍历删除的例子:
for (Integer item:numList){
if (item<=5){
numList.remove(item);
}
}
// 结果会抛异常
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at org.example.LoopTest.method2(LoopTest.java:27)
at org.example.LoopTest.main(LoopTest.java:11)
可以看到在checkForComodification的时候抛出了异常,也就是检查expectedModCount和modCount发现不相等
为什么会抛Itrerator的异常呢?下面就从源码的角度来解释
其实增强for编译后还是使用的Iterator在遍历,反编译的源码如下:
// 增强for循环
for (Integer item:numList){
if (item<=5){
numList.remove(item);
}
}
// 反编译
Iterator var2 = numList.iterator();
while(var2.hasNext()) {
Integer num = (Integer)var2.next();
if (num.<=5) {
nums.remove(num);
}
}
可以看到,增强for遍历的时候使用的还是var2.next() ,但是删除的时候调用的是List本身的remove方法,不是Iterator的删除方法,上面我们提到对List进行任何的修改时 modCount 都会增加,但是Iterator的中expectedModCount的值却没有更新,所以在下一次调用next()的时候检查 expectedModCount和modCount是否相等就会抛异常,这也就是为什么foreach不允许对集合元素进行修改
结论
- 在普通的 for 循环中,可以进行数据的添加操作,但不能进行删除操作。
- 在增强的 for 循环中,既不能进行添加操作,也不能进行删除操作。
- 通过 Iterator 及相关扩展类,可以进行添加或删除操作。