关于foreach:Java list .remove方法仅适用于每个循环内部的倒数第二个对象

Java list's .remove method works only for second last object inside for each loop

本问题已经有最佳答案,请猛点这里访问。

我看到一个奇怪的行为。

1
2
3
4
5
6
7
8
9
10
11
    List<String> li = new ArrayList<>();
    li.add("a");
    li.add("b");
    li.add("c");
    li.add("d");
    li.add("e");
    for(String str:li){
        if(str.equalsIgnoreCase("d")){
            li.remove(str);     //removing second last in list works fine
        }
    }

但是,如果我试图删除列表中倒数第二位以外的任何一个,我会得到ConcurrentModificationException。在阅读"Oracle认证的JavaSee 7程序员学习指南2012"时,我注意到,错误地假定.RevEvE()总是以删除列表中的第二个最后一个例子来工作。


在列表中,添加或删除被视为修改。在你的情况下,你已经5修改(增加)。

"for each"循环的工作原理如下:

1
2
1.It gets the iterator.
2.Checks for hasNext().
1
2
3
4
public boolean hasNext()
{
      return cursor != size(); // cursor is zero initially.
}

3.If true, gets the next element using next().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public E next()
{
        checkForComodification();
        try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
        } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
        }
}

final void checkForComodification()
{
    // Initially modCount = expectedModCount (our case 5)
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

重复步骤2和3,直到hasNext()返回false。

如果我们从列表中删除一个元素,它的大小就会减小,modcount也会增加。

如果我们在迭代时删除一个元素,modcount!=ExpectedModCount满足并抛出ConcurrentModificationException。

但是移除最后一个第二个物体是很奇怪的。让我们看看它在您的案例中是如何工作的。

最初,

cursor = 0 size = 5 --> hasNext() succeeds and next() also succeeds
without exception.
cursor = 1 size = 5 --> hasNext() succeeds and next() also succeeds
without exception.
cursor = 2 size = 5 --> hasNext() succeeds and next() also succeeds
without exception.
cursor = 3 size = 5 --> hasNext() succeeds and next() also succeeds
without exception.

在您的情况下,删除"d",大小将减少到4。

cursor = 4 size = 4 --> hasNext() does not succeed and next() is
skipped.

在其他情况下,ConcurrentModificationException将作为modCount引发!=预期的ODCount。

在这种情况下,不进行这种检查。

如果在迭代时尝试打印元素,则只会打印四个条目。跳过最后一个元素。

希望我说清楚。


这里不要使用List#remove(Object),因为您要从中的列表中为每个循环访问元素。

相反,使用迭代器remove()从列表中删除项:

1
2
3
4
5
6
for(Iterator<String> it=li.iterator(); it.hasNext();) {
    String str = it.next();
    if(str.equalsIgnoreCase("d")) {
        it.remove();     //removing second last in list works fine
    }
}


循环时,请使用Iterator#remove()方法从List中删除元素。在内部,for-each循环将使用Iterator循环通过List,并且由于如果在迭代过程中修改了基础集合(而不是通过调用迭代器的remove()方法),则Iterator的行为是未指定的,因此您得到了异常。

这个循环:

1
2
3
4
5
for(String str:li){
    if(str.equalsIgnoreCase("d")){
       li.remove(str);     //removing second last in list works fine
     }
}

基本上是

1
2
3
4
5
6
7
Iterator<String> itr = li.iterator();
  while(itr.hasNext()){
    String str = (String)itr.next();
    if(str.equalsIgnoreCase("d")){
        li.remove(str);     //removing second last in list works fine
    }
}

为什么删除最后一个第二个元素不会引发异常?

因为通过删除最后一个第二个元素,您已经将大小减少到迭代的元素数。基本的hasNext()实现是

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

因此,在这种情况下,cursor=size=4,所以hasNext()评估为false,并且循环在next()中执行并发修改检查之前提前中断。在这种情况下,不会访问最后一个元素。您可以通过在if中添加一个简单或条件来检查这一点。

1
2
3
4
    if(str.equalsIgnoreCase("d") || str.equalsIgnoreCase("e")){
        // last element"e" won't be removed as it is not accessed
        li.remove(str);  
    }

但是,如果删除任何其他元素,则调用next(),它将抛出ConcurrentModificationException


并发异常是由于arraylist的fail-fast行为引起的。这意味着除了迭代器remove()之外,在迭代列表时不能修改它。

请参阅http://docs.oracle.com/javase/7/docs/api/java/util/arraylist.html


如果您希望全部删除,那么.remove all应该完成这个技巧,而不是遍历集合。我觉得速度也更快。


您可以迭代"向后"并删除元素,但不能向前。因此,不要从第一个元素迭代到最后一个元素,而是从最后一个元素迭代到第一个元素。

伪代码:

1
2
3
4
for(int i = list.size() -1; i >= 0; i--)
{
   list.remove(i);
}


如果您真的想遍历ArrayList并删除元素,那么应该这样做:

1
2
3
4
5
for(int index = yourArrayList.size() - 1; index >= 0; index--) {
    if(yourCondition) {
        yourArrayList.remove(index);
    }
}