¿Qué sucede al eliminar elementos al recorrer la lista?

Introducción

Recientemente escribí un error que eliminaba un elemento interno al recorrer la lista. De hecho, he leído la especificación de desarrollo de Java de Ali antes. Sé que eliminar elementos durante el recorrido causará problemas, pero no me daré cuenta cuando escribo rápido. Ese es exactamente el mecanismo en estudio. Veamos cómo escribir las normas de Ali:

Primero proponga un concepto: extracto de prueba rápida de la Enciclopedia Baidu: el mecanismo de prueba rápida es una especie de mecanismo de error en la colección java (Colección). Cuando varios subprocesos operan en el contenido del mismo conjunto, puede ocurrir un evento de falla rápida. Por ejemplo: cuando un subproceso A atraviesa una colección a través de un iterador, si el contenido de la colección es cambiado por otros subprocesos; luego, cuando el subproceso A accede a la colección, se generará una excepción ConcurrentModificationException, lo que dará como resultado un evento de falla rápida. En resumen, es un mecanismo para que Java evite excepciones concurrentes, pero también se puede generar bajo un solo hilo.

Análisis de ejemplo:

A continuación, exploraré a través de 6 ejemplos, qué sucederá al eliminar elementos al recorrer la lista y su código fuente. La premisa es crear primero una lista:

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

1. Ordinario para bucle

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. Para bucle otra situación

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. Mejorado para 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了");
             }
         }

     }
复制代码

El archivo descompilado puede saber que el mejorado para descompilación usa un iterador para determinar si todavía hay elementos, y luego eliminar usa el método de lista. Veamos cómo hasNext () y next () se escriben en ArrayList. La siguiente es la clase interna Itr en ArrayList implementa la interfaz Iterator,

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

Aquí cursor = i + 1 cuando se ejecuta next (), por lo que cuando se ejecuta al "elemento 2". Igual (elemento) aquí, el elemento 2 se elimina, al iterar sobre el elemento 3 size = 2.cursor = i + 1 = 1 + 1 también es 2. El cursor == tamaño en hasNext () sale directamente.

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

Haga clic para ver el código fuente de 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();
       }
   }
复制代码

Según el mensaje de error, podemos saber que la excepción lanzada por if (modCount! = ExpectedModCount) {throw new ConcurrentModificationException ();}, ¿de dónde vino este elemento modCount? Encontramos AbstractList, que es la clase padre de ArrayList. Mira lo que dice el código fuente

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

Los estudiantes con un buen inglés pueden leerlo por sí mismos, mientras que los estudiantes con un mal inglés pueden abrir silenciosamente Google Translate o Youdao Dictionary.

Los compañeros restantes pueden escuchar mi comprensión. De hecho, solo mirar el primer párrafo básicamente sabe lo que hace. Esto significa que este campo registra el número de cambios estructurales en la lista (podemos entenderlo como agregar, quitar operaciones que cambiarán el tamaño de la lista). Si es causado por otros métodos, generará una excepción ConcurrentModificationException. Así que echemos un vistazo a los métodos de agregar y quitar de la subclase 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
   }    
复制代码

Después de leer estos métodos, ¿ha visto el funcionamiento de modCount ++? Sí, modCount se incrementará en uno al agregar y eliminar. Ok, regresemos y veamos por qué test4 () arroja una excepción. Primero, sabemos que se agregan tres elementos a la lista, por lo que modCount es 3 cuando se agregan elementos, y luego comienza a recorrer. Cuando se encuentra el elemento 2, el elemento 2 se elimina y modCount se convierte en 4. En este momento, lo haremos 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();
       }
   }
复制代码

Se puede ver que modCoun se asigna inicialmente a expectModCount, y luego el bucle for y la última condición if tienen una interrupción en este modCount. Si se encuentra que modCount y expectModCount no son iguales, se lanza una excepción. Al atravesar el elemento 2, action.accept (elementData [i]); Esta línea se ejecuta y luego atraviesa el elemento 3 porque modCount ==pectedModCount no es igual, por lo que se inicia el bucle, por lo que no imprimirá el elemento 3 encontrado , Y la ejecución de la condición if arroja directamente una excepción.

5. Iterador

Echemos un vistazo a la forma correcta de usar iteradores

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

Ya hemos visto el código para este iterador en la prueba 3 anterior, entonces, ¿por qué está bien? De hecho, la escultura de arena es muy fundamental, ¡no lo creas!

Este es el método de eliminación del iterador

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

Por lo tanto, puede encontrar el elemento 3, porque el efecto de eliminar en modCount se ignora directamente.

6.removeIf

A jdk8 también se le ocurrió una nueva forma de escribir, que encapsula la eliminación de elementos al atravesar elementos, removeIf (), de la siguiente manera:

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

Haga clic adentro para ver que el código está casi encapsulado como el anterior.

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

Resumen:

1. Exploramos varias formas de eliminar elementos durante el recorrido, y también conocemos el concepto básico de falla rápida.
2. Aprenda a usar iteradores para eliminar elementos cuando atraviese en el futuro, y descubrió que incluso un solo hilo puede lanzar ConcurrentModificationException
3. Si se trata de una lista de operaciones multiproceso, se recomienda usar CopyOnWriteArrayList o bloquear el iterador. Hábitos personales ~

Supongo que te gusta

Origin juejin.im/post/5e95d282e51d4546dc14b10d
Recomendado
Clasificación