Explain the reasons and solutions of concurrent modification exceptions that occur when ArrayList remove elements are traversed

This article will start with "deleting during traversal", and on the basis of source code analysis and related problem solving. The meaning of modCount, the methods contained in the iterator, and why concurrent modification exceptions occur will all be explained in this article.

Introduce

This is an example of concurrent modification exception. It uses the iterator to get elements, and at the same time uses the remove method of ArrayList itself to remove the elements (the same is true when using the enhanced for loop to traverse to get the elements. The bottom layer of the enhanced for loop is also iteration器,enhanced for loop is nothing but a syntactic sugar over Iterator in Java)

public static void main(String[] args) {
    
    
	//请动手实践运行一下
    List<String> list = new ArrayList<String>();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");
    list.add("e");
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
    
    
        String str = iterator.next();
        if (str.equals("a")) {
    
    
            list.remove(str);
        } else {
    
    
            System.out.println(str);
        }
    }
}

Cause Analysis

The iterator Itr is implemented inside ArrayList, as shown in the figure
Insert picture description here
, when elements are obtained through the iterator (iterator.next()), checkForComodification will be performed. The source code is as follows

public E next() {
    
    
    checkForComodification();/***看这行***/
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;//cursor向后挪一位
    return (E) elementData[lastRet = i];//lastRet为当前取出的元素所在索引,后面会用到
}
final void checkForComodification() {
    
    /***再看这里***/
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

modCount is the number of times this list has been modified by the structure. Structural modifications are those modifications that change the size of the list (such as additions and deletions, note that the size of the list is size rather than capacity), or those that disrupt it in other ways so that the ongoing iteration may produce incorrect results.
The expectedModCount will be initialized to modCount when the iterator is created. The source code is as follows

private class Itr implements Iterator<E> {
    
    
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;
    
    //instance methods...
}   

Did you find any clues? When you call remove (and then call fastRemove), it is regarded as a structural modification, so the value of modCount will change, so that when the program gets the element through iterator.next() again, the checkForComodification method finds that modCount has changed, and expectedModCount It is still the value at initialization, so ConcurrentModificationException is thrown.

Let's confirm our idea, the source code of remove method and fastRemove method are as follows

public boolean remove(Object o) {
    
    
    if (o == null) {
    
    
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
    
    
                fastRemove(index);/***看这行调用了fastRemove***/
                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++;/***再看这行modCount变化了(自增)***/
    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
}

In this way, we have a clear understanding of the concurrent modification exception that occurs in the remove element of the ArrayList during traversal.

Supplementary note for iterator Itr: size is the size of the array, cursor is the index of the next element (although it is the index of the next element, the array start index starts from 0, so the cursor is initialized to 0 by default) The maximum index of the array must be If the index is smaller than size (size-1)=size, it will be out of bounds if the element is still taken.

Before explaining "How to delete data without exception", let me talk about other problems that may arise based on the above example (if you are not interested, you can skip 0.0).

Delete other problems arising from irregularities

Question 1. Although remove is not standardized, the program can still run. Although it does not meet expectations, no concurrent modification exception occurs.
If we delete not a, but d (the maximum is e), abc will be output instead of A concurrent modification exception occurs, the code is as follows

while (iterator.hasNext()) {
    
    
    String str = (String) iterator.next();
    if (str.equals("d")) {
    
    //将原先的a改为d
        list.remove(str);
    } else {
    
    
        System.out.println(str);
    }
}

Reason analysis: because cursor changes from 3 to 4 (counting from 0) when deleting d, and size changes from 5 to 4. Therefore hasNext returns true, and the loop ends, so no e will be output (the end of the loop also means that checkForComodification will not be performed by next, so no exception will be thrown)

Question 2. It seems that no concurrent modification exception will occur, but 0.0 actually occurs.
If we change the element to be deleted to e, then when deleting e, cursor changes from 4 to 5, size changes from 5 to 4, and 5 is obviously greater than There should be no next element after 4, and it won’t enter the loop to fetch the element through next, but when thinking about it this way, an exception occurs. The code is as follows:

while (iterator.hasNext()) {
    
    
    String str = (String) iterator.next();
    if (str.equals("e")) {
    
    
        list.remove(str);
    } else {
    
    
        System.out.println(str);
    }
}

Reason analysis: This is due to the principle of iterator.hasNext(). Click hasNext() to view the source code and you can find that hasNext is not implemented by cursor <size but by cursor != size, so that the program will enter again Looping to fetch elements and concurrent modification exception occurs

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

How to delete elements without reporting errors?

1. Use iterator's remove method to remove elements while obtaining elements through iterator, the code is as follows

while (iterator.hasNext()) {
    
    
    String str = (String) iterator.next();
    if (str.equals("a")) {
    
    
        iterator.remove();
    } else {
    
    
        System.out.println(str);
    }
}

It can be found through the remove source code of Itr (as follows) that it will also update the expectedModCount to the current modCount after it is incremented each time it is deleted, so that it can withstand the checkForComodification check when the element is fetched through iterator.next() next time (just imagine If there is no checkForComodification, the program will continue to loop. The cursor originally points to the next bit of the current element index, but after the remove, the data will move forward as a whole, resulting in a deviation of the data corresponding to the index position pointed to by the cursor In the case of the above question 2, if checkForComodification is not performed, a NoSuchElementException will still occur. For details, see the next source code above).

The value of lastRet is the index corresponding to that element when the element is retrieved through next the latest time. Here, cursor = lastRet is used to move the index of the cursor forward by one bit, thus avoiding the deviation when fetching data (cursor and lastRet See the next source code above for the relationship)

Here lastRet will be classified as -1 (its corresponding element has been deleted), which is why the remove method of the iterator cannot be called twice in succession. If you insist on this, the method will throw an IllegalStateException.

public void remove() {
    
    
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
    
    
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
    
    
        throw new ConcurrentModificationException();
    }
}

2. Get the element through the get method of the list itself and remove the element through its remove method. The code is as follows

for (int i = 0;i<list.size();i++){
    
    
    String s = list.get(i);
    if ("a".equals(s)){
    
    
        list.remove(i);//进而调用fastRemove
        i--;//相当于cursor = lastRet;将返回的下一元素的索引 = 返回的最新元素的索引(当前元素索引)
    }else {
    
    
        System.out.println(s);
    }
}

In this case, the expectedModCount will not be corrected to the latest modCount, and the checkForComodification will not be checked. If the current index is deleted and the current index is deleted at this time, it will cause the aforementioned data deviation (list.size( in the traversal condition) ) Save as a fixed value or continuously call list.remove(i) too many times, an index out-of-bounds exception can also occur 0.0)

add Please refer to the remove method to check the internal class ListItr in ArrayList

Guess you like

Origin blog.csdn.net/weixin_44463178/article/details/108838938