ArrayList cannot add, delete or modify elements with foreach

1, Fail fast

When traversing a collection object with an iterator, if the contents of the collection object are modified (added, deleted, modified) during the traversal, an exception will be thrown ConcurrentModificationException.

one ️⃣ Principle: the iterator directly accesses the contents of the collection during traversal, and uses a modCount variable during traversal. If the content of the collection changes during traversal, the value of modCount will be changed. Whenever the iterator uses hashNext()/next() to traverse the next element, it will detect whether the modCount variable is the expectedmodCount value. If so, it will return the traversal; Otherwise, an exception is thrown and the traversal is terminated.

two ️⃣ Note: the throw condition of the exception here is that modCount is detected= expectedmodCount this condition. If the modified modCount value happens to be set to the expectedmodCount value when the collection changes, no exception will be thrown. Therefore, you cannot program concurrent operations depending on whether this exception is thrown or not. This exception is only recommended to detect concurrent modification bug s.

three ️⃣ Scenario: Java The collection classes under the util package fail quickly and cannot be modified concurrently under multithreading (modified during iteration).

2, Fail safe

The collection container with security failure mechanism is not directly accessed on the collection content during traversal, but copies the original collection content first and traverses the copied collection.

one ️⃣ Principle: since the copy of the original set is traversed during iteration, the modifications made to the original set during traversal cannot be detected by the iterator, so the ConcurrentModificationException will not be triggered.

two ️⃣ Disadvantages: the advantage of copying content is to avoid the ConcurrentModificationException, but similarly, the iterator cannot access the modified content, that is, the iterator traverses the set copy obtained at the moment when it starts traversing, and the iterator does not know the modification of the original set during traversal.

three ️⃣ Scenario: Java util. The containers under the concurrent package are all safe failures and can be used and modified concurrently under multithreading.

3, Lead to problems

one ️⃣ The remove method of listA was executed successfully.

List<String> listA = new ArrayList<String>();
   listA.add("1");
   listA.add("2");
for (String s : listA) {
   if("1".equals(s)){
	  listA.remove(s);
   }
}

two ️⃣ The remove method of listB runs and throws a ConcurrentModificationException.

List<String> listB = new ArrayList<String>();
	listB.add("2");
	listB.add("1");
for (String s : listB) {
	if("1".equals(s)){
	  listB.remove(s);
	}
}

4, To find out why, check the source code

Enhancing the for loop actually depends on the implementation of the while loop and Iterator. All Collection collection classes implement the Iterable interface. Find the Iterator() of the ArrayList class: use its own Itr internal class and implement the Iterator interface.

The essence of an iterator is to call hasNext() to determine whether the next element exists, and then use next() to remove the next element: Itr internal class implementation

/**
 * An optimized version of AbstractList.Itr
 */
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;

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

    @SuppressWarnings("unchecked")
    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;
        return (E) elementData[lastRet = i];
    }

    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();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

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

one ️⃣ listA can remove successfully because it only circulates once. Because after removing element 1, its size - 1 becomes 1, and then the cursor variable inside Itr changes from 0 to 1. At this time, 1 = 1, the cycle ends, so it succeeds.
two ️⃣ listB failed to remove. In fact, it also succeeded in removing the second time of the cycle, but the value of cursor is 2 when judging next for the third time, which makes it not equal to the current size 1, so the next method is executed. The most important thing is that the previous remove operation causes the modCount value of ArrayList to increase by 1, and then the expectedModCount in Itr class remains unchanged, so an exception will be thrown.

Similarly, since the add operation will also cause the modCount to increase automatically, it is not allowed to add, delete or change the elements in the ArrayList in the foreach. For this, it is recommended to use Iterator to delete elements:

Iterator<String> iterator = arrayList2.iterator();
while(iterator.hasNext()){
	String item = iterator.next();
	if("1".equals(item)){
		iterator.remove();
	}
}

If there are concurrent operations, you also need to lock the Iterator.

Keywords: Java Back-end set

Added by anhedonia on Tue, 15 Feb 2022 07:36:43 +0200