What happens when deleting elements while traversing the list?

Introduction:

Recently I wrote a bug that deleted an element inside when traversing the list. In fact, I have read the Java development specification of Ali before. I know that deleting elements during traversal will cause problems, but I will not notice when I write fast. That is exactly the mechanism under study. Let's see how to write Ali norms:

First put forward a concept: fail-fast excerpt from Baidu Encyclopedia: fail-fast mechanism is a kind of error mechanism in java collection (Collection). When multiple threads operate on the contents of the same set, a fail-fast event may occur. For example: when a thread A traverses a collection through an iterator, if the content of the collection is changed by other threads; then when thread A accesses the collection, it will throw a ConcurrentModificationException exception, generating a fail-fast event. In short, it is a mechanism for java to prevent concurrent exceptions, but it can also be generated under a single thread.

Case Analysis:

Next, I will explore through 6 examples, what will happen when deleting elements when traversing the list, and its source code. The premise is to create a list first:

private List<String> list = new ArrayList<String>() {{
       add("元素1");
       add("元素2");
       add("元素3");
   }};
复制代码

1. Ordinary for loop

public void test1() {
       for (int i = 0; i < list.size() - 1; i++) {
           if ("元素3".equals(list.get(i))) {
               System.out.println("找到元素3了");
           }

           if ("元素2".equals(list.get(i))) {
               list.remove(i);
           }
       }
   }
   //  这里不会输出找到元素3 因为遍历到元素2的时候删除了元素2 list的size变小了
   //  所以就产生问题了
复制代码

2. For loop another situation

public void test2() {
       for (int i = 0; i < list.size() - 1; i++) {
           if ("元素2".equals(list.get(i))) {
               list.remove(i);
           }

           if ("元素3".equals(list.get(i))) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 这里会输出元素3 但是其实是在遍历到元素2的时候输出的
   // 遍历到元素2 然后删除 到了判断元素3的条件的时候i是比原来小了1
   // 所以又阴差阳错的输出了正确结果
复制代码

3. Enhanced for loop

public void test3() {
       for (String item : list) {
           if ("元素2".equals(item)) {
               list.remove(item);
           }

           if ("元素3".equals(item)) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 这里和上面的结果有点不一样 但是还是没有输出元素3的打印语句
   // 这里反编译下java文件就可以知道是为啥啦

   public void test3() {
         Iterator var1 = this.list.iterator();

         while(var1.hasNext()) {
           // 为了显示区别这里
             String var2 = (String)var1.next();
             if ("元素2".equals(var2)) {
                 this.list.remove(var2);
             }

             if ("元素3".equals(var2)) {
                 System.out.println("找到元素3了");
             }
         }

     }
复制代码

The decompiled file can know that the enhanced for decompilation uses an iterator to determine whether there are still elements, and then remove uses the list method. Let's see how hasNext () and next () are written in ArrayList. The following is the internal class Itr in ArrayList implements the Iterator interface,

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;

        Itr() {}

        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();
        }
    }
复制代码

Here cursor = i + 1 when executing next (), so when running to "element 2". Equal (item) here, element 2 is removed, when iterating over element 3 size = 2.cursor = i + 1 = 1 + 1 is also 2. Cursor == size in hasNext () directly exits.

4.foreach

public void test4() {
        list.forEach(
                item -> {
                    if ("元素2".equals(item)) {
                        list.remove(item);
                    }

                    if ("元素3".equals(item)) {
                        System.out.println("找到元素3了");
                    }
                }
        );
    }
    // 这里抛出了我们期待已经的fail-fast java.util.ConcurrentModificationException
复制代码

Click to see the source code of ArrayList

@Override
   public void forEach(Consumer<? super E> action) {
       Objects.requireNonNull(action);
       final int expectedModCount = modCount;
       @SuppressWarnings("unchecked")
       final E[] elementData = (E[]) this.elementData;
       final int size = this.size;
       for (int i=0; modCount == expectedModCount && i < size; i++) {
           action.accept(elementData[i]);
       }
       if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
       }
   }
复制代码

According to the error message, we can know that the exception thrown by if (modCount! = ExpectedModCount) {throw new ConcurrentModificationException ();}, where did this modCount element come from? We find AbstractList, which is the parent class of ArrayList. Look at what the source code says

/**
    * The number of times this list has been <i>structurally modified</i>.
    * Structural modifications are those that change the size of the
    * list, or otherwise perturb it in such a fashion that iterations in
    * progress may yield incorrect results.
    *
    * <p>This field is used by the iterator and list iterator implementation
    * returned by the {@code iterator} and {@code listIterator} methods.
    * If the value of this field changes unexpectedly, the iterator (or list
    * iterator) will throw a {@code ConcurrentModificationException} in
    * response to the {@code next}, {@code remove}, {@code previous},
    * {@code set} or {@code add} operations.  This provides
    * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
    * the face of concurrent modification during iteration.
    *
    * <p><b>Use of this field by subclasses is optional.</b> If a subclass
    * wishes to provide fail-fast iterators (and list iterators), then it
    * merely has to increment this field in its {@code add(int, E)} and
    * {@code remove(int)} methods (and any other methods that it overrides
    * that result in structural modifications to the list).  A single call to
    * {@code add(int, E)} or {@code remove(int)} must add no more than
    * one to this field, or the iterators (and list iterators) will throw
    * bogus {@code ConcurrentModificationExceptions}.  If an implementation
    * does not wish to provide fail-fast iterators, this field may be
    * ignored.
    */
protected transient int modCount = 0;
复制代码

Students with good English can read it for themselves. Students with bad English can silently open Google Translate or Youdao Dictionary.

The remaining classmates can listen to my understanding. In fact, just looking at the first paragraph basically knows what it does. This means that this field records the number of structural changes to the list (we can understand it as add, remove operations that will change the size of the list). If it is caused by other methods, it will throw a ConcurrentModificationException exception. So let's take a look at the add and remove methods of the subclass ArrayList.

// 这个是add里面的子方法
private void ensureExplicitCapacity(int minCapacity) {
      modCount++;

      // overflow-conscious code
      if (minCapacity - elementData.length > 0)
          grow(minCapacity);
  }

// 这个是remove(int index)
  public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        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

        return oldValue;
    }
// 这是remove(Object o)
private void fastRemove(int index) {
       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
   }    
复制代码

After reading these methods, have you seen the operation of modCount ++? Yes, the modCount will be increased by one when adding and deleting. Ok, let's go back and see why test4 () throws an exception. First, we know that three elements are added to the list, so modCount is 3 when adding elements, and then it starts to traverse. When element 2 is found, element 2 is removed, and modCount becomes 4. At this time, we will To ArrayList

@Override
   public void forEach(Consumer<? super E> action) {
       Objects.requireNonNull(action);
       final int expectedModCount = modCount;
       @SuppressWarnings("unchecked")
       final E[] elementData = (E[]) this.elementData;
       final int size = this.size;
       for (int i=0; modCount == expectedModCount && i < size; i++) {
           action.accept(elementData[i]);
       }
       if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
       }
   }
复制代码

It can be seen that modCoun is initially assigned to expectedModCount, and then the for loop and the last if condition both have a break on this modCount. If it is found that modCount and expectedModCount are not equal, an exception is thrown. When traversing to element 2, action.accept (elementData [i]); This line is executed, and then traversing element 3 because modCount == expectedModCount is not equal, so the loop is launched, so it will not print out found element 3 , And the execution of the if condition directly throws an exception.

5. Iterator

Let ’s take a look at the correct way to use iterators

public void test5() {
       Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
           String temp = iterator.next();
           if ("元素2".equals(temp)) {
               iterator.remove();
           }

           if ("元素3".equals(temp)) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 这里打印出了 找到元素3了
复制代码

We have already seen the code for this iterator in test3 above, so why is it okay? In fact, the sand sculpture is very principle, don't believe it!

This is the remove method of the iterator

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

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
        // 看到没有直接把modeCount重新赋值给了expectedModCount 所以它们一直会相等啊
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
复制代码

So it can find element 3, because the effect of remove on modCount is directly ignored.

6.removeIf

jdk8 also came up with a new way of writing, which encapsulates the deletion of elements when traversing elements, removeIf (), as follows:

 list.removeIf(item -> "元素2".equals(item)
复制代码

Click inside to see that the code is almost encapsulated just like the previous one.

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;
    }
复制代码

to sum up:

1. We explored several ways to delete elements during traversal, and we also know the basic concept of fail-fast.
2. Learn to use iterators to delete elements when traversing in the future, and found that even a single thread can still throw ConcurrentModificationException
3. If it is a multi-threaded operation list, it is recommended to use CopyOnWriteArrayList, or lock the iterator. Personal habits ~

Guess you like

Origin juejin.im/post/5e95d282e51d4546dc14b10d