Analysis from the source code perspective: Why an exception is reported when deleting data in a Java loop

1. Analysis from the source code perspective: Why an exception is reported when deleting data in a Java loop

I believe everyone has more or less known that deleting data in enhanced for in Java will throw: java.util.ConcurrentModificationException exception, For example: The program is as follows:

public class RmTest {<!-- -->

    public static void main(String[] args) {<!-- -->
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (String l : list) {<!-- -->
            if (Objects.equals(l, "002") || Objects.equals(l,"003")) {<!-- -->
                list.remove(l);
            }
        }
        System.out.println(list);
    }
}

After running, you will find that an exception is thrown:

Especially some novice friends fall into it without paying attention. Of course, the solution is also very simple. You can convert it into an iterator, and then use the iterator’s remove method to delete the data, or use the loop subscript method to pass Delete the subscript, but you need to pay attention to the forward loop and the reverse loop. If it is a forward loop, you need to pay attention to calculating the subscript position, but don’t worry, we will introduce them one by one below.

First, let’s analyze why the java.util.ConcurrentModificationException exception occurs in enhanced for. Here, java is compiled into class form, see in what form the enhanced for is finally executed:

javac RmTest.java

The compiled content is as follows:

public class RmTest {<!-- -->
    public RmTest() {<!-- -->
    }

    public static void main(String[] var0) {<!-- -->
        ArrayList var1 = new ArrayList();
        var1.add("001");
        var1.add("002");
        var1.add("003");
        var1.add("004");
        Iterator var2 = var1.iterator();

        while(true) {<!-- -->
            String var3;
            do {<!-- -->
                if (!var2.hasNext()) {<!-- -->
                    System.out.println(var1);
                    return;
                }

                var3 = (String)var2.next();
            } while(!Objects.equals(var3, "002") & amp; & amp; !Objects.equals(var3, "003"));

            var1.remove(var3);
        }
    }
}

You can see that the enhanced for is finally compiled into an iterator to traverse the data, but it should be noted that deleting data still uses removeList code> method, it can be seen from the exception chain thrown that the problem occurs under the checkForComodification method in the next method:

See below the next method of the iterator under ArrayList, under the Itr class:


In this method, the checkForComodification method is first called. The above exception chain also involves the checkForComodification method. Let’s go to this method:


Have you seen the familiar ConcurrentModificationException exception here? This exception will be thrown as long as modCount and expectedModCount are not equal. Let’s take a look at Where expectedModCount is declared:

It is declared inside the iterator, and the starting value is equal to modCount, while modCount is defined in AbstractList outside the iterator, here also Remember that the data deleted by the remove method in List was used in the previous iterator. Here you can see the method:


The actual deletion logic of this method is in the fastRemove method. Continue to see this method:


Isn’t it very intuitive to see here? The modCount value has changed, but the expectedModCount in the iterator has not been modified accordingly, resulting in expectedModCount != modCount and throw an exception.

We all know that using the remove method in an iterator will not cause an exception, such as:

public class RmTest {<!-- -->

    public static void main(String[] args) {<!-- -->
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {<!-- -->
            String l = iterator.next();
            if (Objects.equals(l, "002") || Objects.equals(l, "003")) {<!-- -->
                iterator.remove();
            }
        }
        System.out.println(list);
    }

}

operation result:

Why is iterator’s remove OK? See the method below:

It can be seen that the remove of the iterator also uses the remove method in List, but it will reset the expectedModCount after deletion. so that it remains consistent with modCount, so the above exception will not be triggered.

After seeing this, you should understand why an exception is thrown, but why is it designed this way? Here we can summarize, modCount mainly represents the number of times the collection has been modified, and expectedModCount represents the number of times the collection maintained internally by the iterator has been modified. When modCount and expectedModCount are not equal, it means that the collection must have been modified somewhere else. At this time, if you continue to use the iterator to traverse the collection, it may An unexpected element is traversed or the next element does not exist. Therefore, as long as expectedModCount and modCount are consistent, the data can be considered credible.

This can also remind us that if we need to operate collections in concurrent situations, we must choose thread-safe collections.

Let me add below that if there is no need to enhance for, is it feasible to delete using the subscript auto-increment method?

public class RmTest {<!-- -->
    public static void main(String[] args) {<!-- -->
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (int i = 0; i < list.size(); i + + ) {<!-- -->
            String l = list.get(i);
            if (Objects.equals(l, "002") || Objects.equals(l,"003")) {<!-- -->
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

After running:

It was found that 003 was not removed, because when 002 was removed, the data after 002 shifted forward, and the original 003 is 2. After shifting, it becomes 1. However, if the subscript i continues to grow, it will How to solve the problem of missing the following data? Since the following data is shifted forward, the subscript i is also shifted forward:

public class RmTest {<!-- -->
    public static void main(String[] args) {<!-- -->
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (int i = 0; i < list.size(); i + + ) {<!-- -->
            String l = list.get(i);
            if (Objects.equals(l, "002") || Objects.equals(l,"003")) {<!-- -->
                list.remove(i);
                i = i-1;
            }
        }
        System.out.println(list);
    }
}

The data is normal after running:

Since traversing the subscripts in the forward direction requires shifting, wouldn’t it be possible to ignore the subscripts if the reverse cycle is reversed:

public class RmTest {<!-- -->
    public static void main(String[] args) {<!-- -->
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (int i = list.size() - 1; i >= 0; i--) {<!-- -->
            String l = list.get(i);
            if (Objects.equals(l, "002") || Objects.equals(l, "003")) {<!-- -->
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

The data is normal after running: