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 remove in
List
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: