Interview - why add and remove elements are not allowed in foreach

1. Foreach uses add and remove in the process of traversing the ArrayList

Let's first take a look at the results of using add and remove in the process of using foreach to traverse the ArrayList, and then analyze it.

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
        list.add(i);
    }
    for (Integer j : list) {
        if (j.equals(3)) {
            list.remove(3);
        }
        System.out.println(j);
    }
}

operation result:

0
1
2
3
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 test.Test.main(Test.java:12)

The result is a ConcurrentModificationException exception, and track the location where the exception is thrown (ArrayList.java:911)

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, so what do these two parameters represent? Why is there an exception when they are not equal?

2. Tracing the source

2.1. What is modCount?

At this time, let us look at the source code. When we click on this variable, there will be a comment telling us that modCount is a member variable in the AbstractList class, and the value indicates the number of modifications to the List.

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

As you can see, in the remove method, in fact, only ++ is performed on modCount, so what is expectedModCount?

2.2. What is expectedModCount?

expectedModCount is an internal class in ArrayList—a member variable in Itr. Let's see how to pull out another internal class Itr.

Through decompilation, it can be found that foreach is implemented internally using iterators after compilation.

 The iterator is instantiated through list.iterator(), and list.iterator() returns an object of the internal class Itr. From the source code, it can be seen that Itr implements the Iterator interface and declares the member variable expectedModCount, which means expectedModCount The expected value of the modification times of ArrayList, its initial value is modCount.

2.3. The familiar checkForComodification method

From the source code, we can see that the next and remove methods of this class call a checkForComodification method. Are you familiar with checkForComodification? Isn’t this the place where the exception is thrown?

The checkForComodification method determines whether to throw a concurrent modification exception by judging whether modCount and expectedModCount are equal.

2.4. Process review

By looking at the compiled class file, we can see that the general process is as follows: when j is 3, the remove method is called, the modCount value is modified in the remove method, and then the value of j is output, and then enters the next cycle, at this time hasNext is true, enter the first line of code in the loop body, call the next method, and then call the checkForComodification method, and then find that the expectedModCount and modCount are inconsistent, and finally throw a ConcurrentModificationException.

 That is to say, expectedModCount is initialized to modCount, but expectedModCount is not modified later, and modCount is modified in the process of remove and add, which leads to the checkForComodification method to judge whether the two values ​​are equal during execution, and if they are equal , then no problem, if they are not equal, then an exception will be thrown for you.

And this is what we call the fail-fast mechanism in layman's terms, that is, the rapid detection failure mechanism.

3. Avoid fail-fast mechanism

3.1. Use listIterator or iterator

The fail-fast mechanism can also be avoided, for example, take out our above code

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        list.add(i);
    }

    System.out.println("没有删除元素前"+list.toString());
    // 迭代器使用listIterator和iterator均可
    ListIterator<Integer> listIterator = list.listIterator();
    while(listIterator.hasNext()){
        Integer integer = listIterator.next();
        if(integer==3){
            listIterator.remove();
            listIterator.add(9);
        }
    }
    System.out.println("删除元素后"+list.toString());
}

 In this case, you will find that it can be run, and there is no problem. Let's see the running result:

没有删除元素前[0, 1, 2, 3, 4]
删除元素后[0, 1, 2, 9, 4]

 The result is also obvious, we have implemented the operations of add and remove in foreach.

There is a point to note here. Both listIterator and iterator can be used for the iterator. You can see the internal class of ListItr actually used by listIterator by looking at the source code. ListItr inherits the Itr class and seals some methods, such as add, hasPrevious, previous, etc. . So the remove method in the code is of the Itr class, and the add method is of the ListItr class

 The difference between listIterator and iterator:

  1. The scope of use is different, Iterator can be applied to all collections, Set, List and Map and subtypes of these collections. And ListIterator can only be used for List and its subtypes.
  2. ListIterator has an add method that can add objects to the List, but Iterator cannot.
  3. Both ListIterator and Iterator have hasNext() and next() methods, which can realize sequential backward traversal, but ListIterator has hasPrevious() and previous() methods, which can realize reverse (order forward) traversal. Iterators cannot.
  4. ListIterator can locate the position of the current index, and nextIndex() and previousIndex() can be implemented. Iterator does not have this functionality.
  5. Both can implement deletion operations, but ListIterator can implement object modification, and the set() method can implement it. Iterator can only be traversed and cannot be modified.

3.2. Use CopyOnWriteArrayList

The CopyOnWriteArrayList class can also solve the fail-fast problem. Let's try it:

public static void main(String[] args) {
    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    for (int i = 0; i < 5; i++) {
        list.add(i);
    }
    System.out.println("没有删除元素前"+list.toString());
    for (Integer integer : list) {
        if(integer.equals(3)){
            list.remove(3);
            list.add(9);
        }
    }
    System.out.println("删除元素后"+list.toString());
}

 operation result:

没有删除元素前[0, 1, 2, 3, 4]
删除元素后[0, 1, 2, 4, 9]

CopyOnWriteArrayList implements the operation of removing and adding in the middle of this element, so how is its internal source code implemented, it is actually very simple, copy

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

Because you use copying, there must be two spaces that store the same content, which consumes space. When the GC is finally performed, does it take some time to clean it up, so I personally don’t recommend it, but writing There is still a need to come out.

3.2.1, the add method of CopyOnWriteArrayList

public boolean add(E e) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 元素数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 复制数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 存放元素e
        newElements[len] = e;
        // 设置数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

The processing flow is as follows:

  • Acquire the lock (to ensure safe access from multiple threads), obtain the current Object array, obtain the length of the Object array as length, and proceed to step ②.

  • Copy an Object array with a length of length+1 as newElements according to the Object array (at this time, newElements[length] is null), and enter the next step.

  • Set the array element newElements[length] with subscript length as element e, then set the current Object[] as newElements, release the lock, and return. This completes the addition of elements.

3.2.2, the remove method of CopyOnWriteArrayList

public E remove(int index) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 获取旧值
        E oldValue = get(elements, index);
        // 需要移动的元素个数
        int numMoved = len - index - 1;
        if (numMoved == 0) // 移动个数为0
            // 复制后设置数组
            setArray(Arrays.copyOf(elements, len - 1));
        else { // 移动个数不为0
            // 新生数组
            Object[] newElements = new Object[len - 1];
            // 复制index索引之前的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            // 复制index索引之后的元素
            System.arraycopy(elements, index + 1, newElements, index,
                                numMoved);
            // 设置索引
            setArray(newElements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

The processing flow is as follows:

  1. Get the lock, get the array elements, the length of the array is length, get the index value elements[index], calculate the number of elements that need to be moved (length - index - 1), if the number is 0, it means that the removed is the array For the last element, copy the elements array, the copy length is length-1, then set the array, and go to step ③; otherwise, go to step ②
  2. First copy the elements before the index index, then copy the elements after the index index, and then set the array.
  3. Release the lock, returning the old value.

Notice

CopyOnWriteArrayList solves the problem of fail-fast not by removing or adding elements through iterators, but through the remove and add methods of the list itself, so the position of the added elements is also different, the iterator is the one behind the current position, and CopyOnWriteArrayList is directly placed to the end.

Students who have ideas can take a look at the listIterator and iterator of CopyOnWriteArrayList. They are actually the same, and they are all returned internal classes of COWIterator.

 The remove, set, and add operations are not supported in the internal class of COWIterator, at least the jdk1.8 I use does not support it, and an UnsupportedOperationException will be thrown directly:

 I will write here first, and I will add later when I have time.

Guess you like

Origin blog.csdn.net/qq_34272760/article/details/120953988