List遍历删除与迭代器(Iterator)

List遍历删除与迭代器(Iterator)

原文地址:https://www.cnblogs.com/wunsiang/p/12765144.html
List集合使我们非常熟悉的,ArrayList等集合为我们提供了remove()方法,但在遍历时却不能随便使用,我们我们今天便从实现层面讨论下原因以及Iterator的相关知识。

ArrayList 遍历时删除方法
for循环向后遍历的陷阱

for(int i=0;i<list.size();i++){
    if(list.get(i).equals("del"))
        list.remove(i);
}

从前向后for循环遍历同时如果调用ArrayList提供的remove方法的话主要你删除第一个元素后会导致后面的元素向前移动,比如你删除了第0个元素后后面的n-1个元素都向前移动一个位置,但是i的值变为了1,而实际上一开始位于index=1位置的元素已经被移动到了index=0位置上,导致漏掉部分元素。

解决办法

从list最后1个元素开始从后向前遍历。

for(int i=list.size()-1;i>=0;i--){
    if(list.get(i).equals("del"))
        list.remove(i);

增强型for循环(foreach)遇到的问题

for(String s:list){  
    if(s.equals("two")){  
        list.remove(s);  
    }  
}


如上代码运行会报错如下

Exception in thread "main" java.util.ConcurrentModificationException  
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)  
    at java.util.AbstractList$Itr.next(AbstractList.java:343)  
    at Test.main(Test.java:22)  

为什么会突然报错?我们先考虑一个问题,什么是foreach?

通过对class文件反编译,我们可以发现对于List集合,foreach实际上是调用了itearator()方式通过迭代器进行遍历。那思路就清晰了,我们来看一看ArrayList实现的Itr迭代器的next()方法源码

// ArrayList.java#Itr

public E next() {
    // 校验是否数组发生了变化
    checkForComodification();
    // 判断如果超过 size 范围,抛出 NoSuchElementException 异常
    int i = cursor; // <1> i 记录当前 cursor 的位置
    if (i >= size)
        throw new NoSuchElementException();
    // 判断如果超过 elementData 大小,说明可能被修改了,抛出 ConcurrentModificationException 异常
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    // <2> cursor 指向下一个位置
    cursor = i + 1;
    // <3> 返回当前位置的元素
    return (E) elementData[lastRet = i]; // <4> 此处,会将 lastRet 指向当前位置
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

我们在这发现了抛出的异常,也看到了抛出异常的原因modCount != expectedModCount,这个modCount和expectedModCount是怎么回事呢,我们先来看看expectedModCount

// ArrayList.java#Itr

/**
 * 下一个访问元素的位置,从下标 0 开始。
 */
int cursor;       // index of next element to return
/**
 * 上一次访问元素的位置。
 *
 * 1. 初始化为 -1 ,表示无上一个访问的元素
 * 2. 遍历到下一个元素时,lastRet 会指向当前元素,而 cursor 会指向下一个元素。这样,如果我们要实现 remove 方法,移除当前元素,就可以实现了。
 * 3. 移除元素时,设置为 -1 ,表示最后访问的元素不存在了,都被移除咧。
 */
int lastRet = -1; // index of last element returned; -1 if no such
/**
 * 创建迭代器时,数组修改次数。
 *
 * 在迭代过程中,如果数组发生了变化,会抛出 ConcurrentModificationException 异常。
 */
int expectedModCount = modCount;

// prevent creating a synthetic constructor
Itr() {}

从源码中我们可以知道,expectedModCount是Itr的1个属性,记录创建迭代器时数组的修改次数。

我们再来看看modCount又是在哪发生变化的呢?

// ArrayList.java
public E remove(int index) {
    // 校验 index 不要超过 size
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    // 记录该位置的原值
    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    // <X>快速移除
    fastRemove(es, index);

    // 返回该位置的原值
    return oldValue;
}

private void fastRemove(Object[] es, int i) {
    // 增加数组修改次数
    modCount++;
    // <Y>如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪
    final int newSize;
    if ((newSize = size - 1) > i) // -1 的原因是,size 是从 1 开始,而数组下标是从 0 开始。
        System.arraycopy(es, i + 1, es, i, newSize - i);
    // 将新的末尾置为 null ,帮助 GC
    es[size = newSize] = null;
}

至此我们就明白了,ArrayList在进行增加/删除操作时会对modCount进行修改,记录修改次数,这本没什么问题,但使用itearator遍历时会进行checkForComodification()操作,从而导致modCount != expectedModCount抛出ConcurrentModificationException。

整体来说,也就是Iterator遍历时不允许并发调用ArrayList的remove/add操作进行修改,否则会抛出异常。

那我们应该怎样在遍历时进行增改操作呢?

使用迭代器进行遍历同时修改操作


Iterator<String> it = list.iterator();
while(it.hasNext()){
    String x = it.next();
    if(x.equals("del")){
        it.remove();
    }
}

如此我们便可以正常的循环及删除。可能有同学还会有疑虑为什么这样不会抛出刚才的异常呢?我们仍然可以从Itr类的remove()方法源码中找到答案。

// ArrayList.java#Itr

public void remove() {
    // 如果 lastRet 小于 0 ,说明没有指向任何元素,抛出 IllegalStateException 异常
    if (lastRet < 0)
        throw new IllegalStateException();
    // 校验是否数组发生了变化
    checkForComodification();

    try {
        // <1> 移除 lastRet 位置的元素
        ArrayList.this.remove(lastRet);
        // <2> cursor 指向 lastRet 位置,因为被移了,所以需要后退下
        cursor = lastRet;
        // <3> lastRet 标记为 -1 ,因为当前元素被移除了
        lastRet = -1;
        // <4> 记录新的数组的修改次数
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

原来,Itr提供的remove方法也是调用了ArrayList的remove方法,但是他在调用之后还修改了expectedModCount的值,这样就可以在遍历过程中分清修改操作的“敌我”啦。

iterator调用remove()方法为什么要先调用next()方法?
这也是在使用iterator时可能会让部分同学感到困惑的问题。我们看一看上文中的Itr#remove()方法可以发现,它实际上是删除lastRet指向的元素,而lastRet在每次remove调用后会默认置为-1,并将cursor指针向前走一个位置(因为由于删除元素接下来要把被删除元素后面的所有数组元素向前挪一个位置)。接下来我们再看next()方法源码,next调用后会将lastRet指向上个元素的索引,cursor指向下一个位置,所以调用remove()方法要先调用next()方法,注意,next方法返回值为上一个元素的值。

所以我们可以总结下,next方法返回的为上一个元素的值,remove删的也是上一个元素。cursur指向的是后一个元素,在发生remove后,cursor会回退一个位置从而保证遍历不漏元素。因此,也就不难理解hasNext()方法的实现逻辑了。

// ArrayList.java#Itr

public boolean hasNext() {
    return cursor != size;
}

源码看完了,我们再回过头来看看概念,相信就好理解多啦。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值