Cannot foreach delete / add reason for List collection

ArrayList

Let's first look at the results in ArrayList if we use add and remove, and then let's analyze them.

public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //Put the elements in the list
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i + "");
        }
        for (String s: list) {
            if ("5".equals(s)){
                list.remove(5);
            }
            System.out.println(s);
        }
    }

Let's see what the results look like first.

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 this time, someone said, why don't you use the iterator iterator directly? In fact, those who say this generally haven't seen the source code. Why do you say so? If you decompile the foreach code, you must find that it is implemented internally using iterators. In that case, let's try traversing it with iterators.

public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        //Put the elements in the list
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i);
        }
       Iterator<Integer> iterator = list.iterator();
           while(iterator.hasNext()){
               Integer integer = iterator.next();
               if(integer==5){
                   list.remove();   //Pay attention to this place
               }
           }
    }

So what happened? The result is the same, or there will be exceptions.

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)

The same exception ConcurrentModificationException appears. Since it has prompted us the location of the exception, let's take a look at what it looks like in the source code of ArrayList.

Abnormal location

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

This place tells us that if modCount is not equal to expectedModCount, this exception message will be thrown. What do these two parameters represent? Why do exceptions occur when they are not equal?

At this time, let's look at the source code. When we click this variable, a comment will tell us that modCount is a member variable in the AbstractList class, and this value represents the number of modifications to the List

At this time, let's see if this variable is increased or decreased in the remove method.

public E remove(int index) {
        rangeCheck(index); //Check whether the index is legal

        modCount++; //modCout direct++
        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;
    }

 public boolean remove(Object o) {
        if (o == 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);
                    return true;
                }
        }
        return false;
    }

private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; //  Set to null
    }

As you can see, in the remove method, only the modCount is actually + +, what is expectedModCount?

To delete an element through the remove method, the fastRemove() method is finally called. In the fastRemove() method, first add 1 to the modCount (because the collection is modified once), then delete the element, and finally subtract 1 from the size and set the reference to null to facilitate the garbage collector's collection.

expectedModCount is an internal class in ArrayList -- a member variable in Itr.

Let's look for the source code.

int expectedModCount = modCount;

expectedModCount represents the expected number of modifications to ArrayList, and its initial value is modCount.

Let's take a look at how the inner class in ArrayList assigns a value to it. After all, its initial value is modCount, and this inner class is iterator.

From the source code, we can see that the next and remove methods of this class call a checkForComodification method. It determines whether to throw concurrent modification exceptions by judging whether modCount and expectedModCount are equal

final int expectedModCount = modCount;

In other words, the expectedModCount is initialized to modCount, but the expectedModCount is not modified later. In the process of remove and add, the modCount is modified, which leads to whether the two are equal through the checkforcodification method. If they are equal, there is no problem. If they are not equal, Then throw you an exception.

This is our popular fail fast mechanism, that is, the rapid failure detection mechanism.

This fail fast mechanism can also be avoided. For example, take out the code above,

public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        //Put the elements in the list
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i);
        }
        System.out.print("Before deleting an element"+list.toString());
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==5){
                iterator.remove();   //Pay attention to this place
            }
        }
        System.out.print("After deleting an element"+list.toString());
    }

In this way, you will find that it can run and has no problem. Let's look at the running results:

Before deleting an element[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

After deleting an element[0, 1, 2, 3, 4, 6, 7, 8, 9]

The result is also obvious. We implement the add and remove operations in foreach

In fact, there is another way, CopyOnWriteArrayList. This class can also solve the problem of fail fast. Let's try it,

public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        //Put the elements in the list
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i);
        }
        System.out.print("Before deleting an element"+list.toString());
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==5){
                list.remove(5);   //Pay attention to this place
            }
        }
        System.out.print("After deleting an element"+list.toString());
    }

After we run, the result is the same,

Before deleting an element[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

After deleting an element[0, 1, 2, 3, 4, 6, 7, 8, 9]

He has implemented the operation of removing the middle of this element. How to implement his internal source code is actually very simple and easy to copy

That is, he creates a new array and then copies the old array to the new array, but the fundamental reason why few people recommend this approach is   copy

Because you use replication, there must be two spaces for storing the same content, which consumes space. Finally, when performing GC, does it also take some time to clean it up, so ah fan doesn't recommend it personally, but it's still necessary to write it out. After all, it's personal advice, Do you see how to remove in foreach?

Keywords: Java data structure list

Added by jpoladsky on Sat, 23 Oct 2021 04:43:51 +0300