Entrevistador: ¿Por qué HashMap no puede atravesar y eliminar?

 
  
 
  
 
  
您好,我是路人,更多优质文章见个人博客:http://itsoku.com

Hace algún tiempo, cuando mi colega escaneó KW en el código, apareció este mensaje:

5c17537eb6b72500105bdcc455da93c3.png

La razón de esto anterior es que cuando se usa foreach para atravesar HashMap, habrá problemas con la operación de asignación de colocación al mismo tiempo y se producirá la excepción ConcurrentModificationException.

Entonces lo miré brevemente y mi impresión es que la clase de colección debe tener cuidado al eliminar o agregar operaciones durante el recorrido. Generalmente, los iteradores se usan para las operaciones.

Entonces les dije a mis colegas que el Iterador debería usarse para operar en los elementos de la colección. Los colegas me preguntaron por qué. ¿Ahora estoy confundido? Sí, solo recuerdo si se puede usar así, pero parece que nunca he investigado por qué.

¡Así que hoy decidí estudiar detenidamente esta operación transversal de HashMap para evitar la minería a cielo abierto!

bucle foreach?

La sintaxis foreach de Java es una nueva característica agregada en JDK 1.5. Se utiliza principalmente como una mejora de la sintaxis for, entonces, ¿cómo se implementa su capa inferior? Miremos más de cerca:

Dentro de la sintaxis foreach, la colección se implementa mediante el iterador y la matriz se implementa mediante el recorrido de subíndices. Los compiladores de Java 5 y superiores ocultan la implementación interna basada en la iteración y el recorrido de subíndices de matriz.

Nota: Lo que se dice aquí es que el "compilador Java" o el lenguaje Java oculta su implementación, no que un determinado fragmento de código Java oculta su implementación, es decir, no podemos encontrarlo en ningún fragmento de código Java JDK. La implementación está oculta hasta aquí. La implementación aquí está oculta en el compilador de Java. Mire el código de bytes compilado por un código Java para cada uno y adivine cómo se implementa.

Escribamos un ejemplo para estudiar:

public class HashMapIteratorDemo {
    String[] arr = {
        "aa",
        "bb",
        "cc"
    };


    public void test1() {
        for (String str: arr) {}
    }
}

Convierta el ejemplo anterior en código de bytes y descompílelo (parte de la función principal):

da15ff91242e8810dfdfcfb91d02f234.png

Tal vez no podamos entender claramente qué hacen estas instrucciones, pero podemos comparar las instrucciones de código de bytes generadas por el siguiente código:

 
  
public class HashMapIteratorDemo2 {
    String[] arr = {
        "aa",
        "bb",
        "cc"
    };


    public void test1() {
        for (int i = 0; i < arr.length; i++) {
            String str = arr[i];
        }
    }
}

42da63878c40537f82f5955a94b0356b.png

Mire los dos archivos de código de bytes, ¿encuentra que las instrucciones son casi las mismas? Si aún tiene dudas, veamos la operación foreach en la colección:

Recorre la colección con foreach:

 
  
public class HashMapIteratorDemo3 {
    List < Integer > list = new ArrayList < > ();


    public void test1() {
        list.add(1);
        list.add(2);
        list.add(3);


        for (Integer
            var: list) {}
    }
}

Recorre la colección a través de Iterator:

 
  
public class HashMapIteratorDemo4 {
    List < Integer > list = new ArrayList < > ();


    public void test1() {
        list.add(1);
        list.add(2);
        list.add(3);


        Iterator < Integer > it = list.iterator();
        while (it.hasNext()) {
            Integer
            var = it.next();
        }
    }
}

Compare los códigos de bytes de los dos métodos de la siguiente manera:

5c68aa3b2c3afea6227090ac2eea0b3c.png

64a60e17da6762dab9f65895c93b0764.png

Descubrimos que las instrucciones de código de bytes de los dos métodos funcionan casi exactamente igual;

Así podemos sacar las siguientes conclusiones:

Para las colecciones, dado que todas las colecciones implementan iteradores Iterator, el compilador finalmente convierte la sintaxis foreach en una llamada a Iterator.next();

Para una matriz, se transforma en una referencia circular a cada elemento de la matriz.

HashMap atraviesa la colección y elimina, coloca y agrega los elementos de la colección.

1. Fenómeno

Según el análisis anterior, sabemos que la capa inferior de HashMap implementa el iterador Iterator, por lo que en teoría también podemos usar el iterador para atravesar, lo cual es cierto, por ejemplo de la siguiente manera:

 
  
public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");


        for (Map.Entry < Integer, String > entry: map.entrySet()) {
            int k = entry.getKey();
            String v = entry.getValue();
            System.out.println(k + " = " + v);
        }
    }
}

producción:

a45723b139a1629e89c93962b9e5b7c0.png

Bien, no hay ningún problema con el recorrido, entonces, ¿qué tal si eliminamos, colocamos y agregamos elementos del conjunto de operaciones?

 
  
public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");


        for (Map.Entry < Integer, String > entry: map.entrySet()) {
            int k = entry.getKey();
            if (k == 1) {
                map.put(1, "AA");
            }
            String v = entry.getValue();
            System.out.println(k + " = " + v);
        }
    }
}

Resultados de:

ef1a32e78fd704d2cf279d991edfcd7d.png

No hay ningún problema con la ejecución y la operación de venta también es exitosa.

¡pero! ¡pero! ¡pero! ¡Aquí viene el problema! ! !

Sabemos que HashMap es una clase de colección insegura para subprocesos. Si usa foreach para atravesar, agregar y eliminar operaciones provocará una excepción java.util.ConcurrentModificationException. La operación de venta puede generar esta excepción. (Por qué es posible, lo explicaremos más adelante)

¿Por qué se lanza esta excepción?

Echemos un vistazo a la explicación de la operación HasMap en la documentación de la API de Java.

f524645f55ab0f8b2ba583323a33c372.png

La traducción significa aproximadamente: Este método devuelve una vista de colección de las claves contenidas en este mapa.

Las colecciones están respaldadas por mapas, y si el mapa se modifica mientras se itera sobre la colección (que no sea mediante las operaciones de eliminación del propio iterador), los resultados de la iteración no están definidos. Las colecciones admiten la eliminación de elementos, que elimina la asignación correspondiente del mapa mediante las operaciones Iterator.remove, set.remove, removeAll, retener y borrar. En pocas palabras, al atravesar una colección a través de map.entrySet (), operaciones como eliminar y agregar no se pueden realizar en la colección en sí, y es necesario utilizar iteradores para las operaciones.

Para la operación put, si la operación es una operación de reemplazo que modifica el primer elemento como en el ejemplo anterior, no se generará ninguna excepción, pero si la operación es para agregar elementos usando put, definitivamente se generará una excepción. Modifiquemos el ejemplo anterior:

public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");


        for (Map.Entry < Integer, String > entry: map.entrySet()) {
            int k = entry.getKey();
            if (k == 1) {
                map.put(4, "AA");
            }
            String v = entry.getValue();
            System.out.println(k + " = " + v);
        }
    }
}

Se produjo una excepción durante la ejecución:

596efd1a4c45e697c7b22383cadf634e.png

Esto es para verificar que la operación de colocación mencionada anteriormente pueda generar una excepción java.util.ConcurrentModificationException.

Pero tengo dudas: ¿Dijimos anteriormente que el bucle foreach se atraviesa a través de iteradores? ¿Por qué es imposible venir aquí?

En realidad, esto es muy simple. La razón es que la capa inferior de nuestra operación transversal se realiza a través de iteradores, pero nuestra operación de eliminación y otras operaciones se realizan operando directamente el mapa, como en el ejemplo anterior: map.put(4, " AA"); // La operación real aquí es directamente en la colección, en lugar de a través del iterador. Por lo tanto, todavía habrá excepciones de ConcurrentModificationException.

2. Estudie los principios subyacentes en detalle.

Veamos nuevamente el código fuente de HashMap. A través del código fuente, encontramos que este método se usa cuando la colección se recorre usando Iterator:

final Node < K, V > nextNode() {
    Node < K, V > [] t;
    Node < K, V > e = next;
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    if ((next = (current = e).next) == null && (t = table) != null) {
        do {} while (index < t.length && (next = t[index++]) == null);
    }
    return e;
}

Aquí modCount indica cuántas veces se han modificado los elementos en el mapa (este valor aumentará automáticamente al eliminar y agregar nuevos elementos), y expectedModCount indica el número esperado de modificaciones, y estos dos valores son iguales cuando se construye el iterador, si los dos valores no están sincronizados durante el proceso transversal, se generará una excepción ConcurrentModificationException.

Ahora veamos la operación de eliminación de colección:

(1) La implementación de eliminación del propio HashMap:

65fa6f67b6f8289f33c4568720067661.png

 
  
public V remove(Object key) {
    Node < K, V > e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

(2) La implementación de eliminación de HashMap.KeySet

 
  
public final boolean remove(Object key) {
    return removeNode(hash(key), key, null, false, true) != null;
}

(3) La implementación de eliminación de HashMap.EntrySet

 
  
public final boolean remove(Object o) {
    if (o instanceof Map.Entry) {
        Map.Entry << ? , ? > e = (Map.Entry << ? , ? > ) o;
        Object key = e.getKey();
        Object value = e.getValue();
        return removeNode(hash(key), key, value, true, true) != null;
    }
    return false;
}

(4) Implementación del método de eliminación de HashMap.HashIterator

public final void remove() {
    Node < K, V > p = current;
    if (p == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    current = null;
    K key = p.key;
    removeNode(hash(key), key, null, false, false);
    expectedModCount = modCount; //--这里将expectedModCount 与modCount进行同步
}

Todos los cuatro métodos anteriores implementan la operación de eliminar la clave llamando al método HashMap.removeNode. Siempre que se elimine la clave en el método removeNode, modCount realizará una operación de incremento automático y modCount será inconsistente con el esperadoModCount en este momento;

final Node < K, V > removeNode(int hash, Object key, Object value,
    boolean matchValue, boolean movable) {
    Node < K, V > [] tab;
    Node < K, V > p;
    int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        ...
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode < K, V > ) node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount; //----这里对modCount进行了自增,可能会导致后面与expectedModCount不一致
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

Entre las tres implementaciones de eliminación anteriores, solo el método de eliminación del tercer iterador sincroniza el valor esperadoModCount con modCount después de llamar al método removeNode, por lo que al atravesar el siguiente elemento y llamar al método nextNode, el método del iterador no generará una excepción.

¿Hay aquí una sensación de comprensión repentina?

Por lo tanto, si necesita realizar operaciones de elementos en el recorrido de la colección, debe utilizar el iterador Iterator, de la siguiente manera:

public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map < Integer, String > map = new HashMap < > ();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");


        Iterator < Map.Entry < Integer, String >> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry < Integer, String > entry = it.next();
            int key = entry.getKey();
            if (key == 1) {
                it.remove();
            }
        }
    }
}
↓↓↓ 点击阅读原文,直达个人博客
 你在看吗

Supongo que te gusta

Origin blog.csdn.net/likun557/article/details/131629923
Recomendado
Clasificación