List traversal to delete elements remove()

Today I encountered the problem of traversing the remove list according to the conditions. The first time was a simple for loop remove. I only knew that it was not possible to write this way and it was unsafe, but why? Have you thought about it? Let's dig deeper on the issue of List traversal remove!

1. Several common traversal methods

1. Ordinary for loop

2. Advanced for loop

3、iterator和removeIf

4、stream()

5. Copy

6. Ordinary for loop --> Reverse order

2. Source code

1. Reasons for errors in ordinary for loops

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
/*
 * Private remove method that skips bounds checking and does not
 * return the value removed.
 */
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //remove会导致之后的元素往前移动,而下标不改变时就会出现bug
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

After we delete an element, the size of the list has changed. At this time, your index will also change, which will cause you to miss some elements when traversing.
For example, after you delete the first element, if we continue to access the second element based on the index, because of the deletion, the following elements have moved one place forward, so the actual access is the third element.
So this method can be used when deleting a specific element, but it is not suitable for deleting multiple elements in a loop.

2. Reasons for errors in advanced for loops

Foreach actually uses iterators to traverse, and what problems can be caused by directly using the remove method of arraylist when traversing?

You can look at the internal code of fastremove and iterator traversal:

In fact, this is what caused the above exception to be thrown. Simply put, calling the list.remove() method causes the values ​​of modCount and expectedModCount to be inconsistent and an exception is reported. 

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
//调用next时会调用checkForComodification方法检查 这两个字段
//而fastRemove里面只对modCount 进行了改变 导致抛出异常
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];
}

So remove does not apply to foreach during traversal.

3, java8 new method removeIf

//内部其实就是迭代器遍历
default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}

Similar to an iterator, the internal implementation is also an iterator.

Three, summary

1. It is faster to copy a new list without considering whether or not the memory size will appear OOM. It is suitable for when there are not many objects in the collection. After all, only the add operation is required.

2. When there are too many elements in the collection, copying the list becomes a bit cumbersome. It is faster to traverse using the iterator, and there is no need to pay attention to the change of the small subscript.

3. Use the removeIf method when performance is not considered, the code is concise and clear.

4. When you want to remove elements for the corner mark, it is most appropriate to use the reverse traversal method.

 

Previous: Summary of Java basic knowledge (absolutely classic)

Next: Summary of Java interview questions (with answers)

Guess you like

Origin blog.csdn.net/guorui_java/article/details/110098348