Comprenda a fondo los principios subyacentes de HashMap

HashMapDefinitivamente es la colección más utilizada y una de las más solicitadas en las entrevistas.Sólo conociendo a fondo todos los puntos puedes estar seguro y cómodo cuando te enfrentas a la gran fábrica. Luego te lleva a destapar lentamente HashMapel velo.

1. Haz una pregunta

La mejor manera de aprender un punto de conocimiento es aprender con preguntas. Luego, primero lanzamos algunas preguntas comunes en las entrevistas y luego analizaremos HashMaplos principios de estas preguntas poco a poco .

  • ¿La estructura de datos subyacente de HashMap?
  • ¿Cuál es la diferencia entre Java7 y Java8?
  • ¿Por qué el hilo no es seguro?
  • ¿Hay alguna clase segura para subprocesos en su lugar?
  • ¿Cuál es el tamaño de inicialización predeterminado? ¿Por qué hay tantos? ¿Por qué el tamaño es una potencia de 2?
  • ¿Cómo expandir HashMap? ¿Qué es el factor de carga? ¿Por qué hay tanto?
  • ¿Cómo maneja HashMap las colisiones de hash?
  • ¿Reglas de cálculo hash?

2. La estructura de datos subyacente de HashMap

HashMapLa estructura de datos subyacente de está principalmente en forma de 数组+ 链表, JDK8que también se utilizará en 红黑树. La estructura específica se muestra en la siguiente figura:Inserte la descripción de la imagen aquí

¿Por qué HasMap usa la estructura de datos de matriz + lista vinculada? ¿Por qué se introduce el árbol rojo-negro en JDK8?

1. Por qué utilizar matrices.

Sabemos que la ventaja de las matrices es que los elementos correspondientes se pueden encontrar rápidamente en función del subíndice. En la HashMaplata de acuerdo con keyel hashCodevalor calculado, donde se encuentra el subíndice de una matriz, es más rápido a la ubicación del nodo.

2. ¿Por qué necesita una lista vinculada?

JavaEl hashCodetipo es intel tipo, que es el rango . Con una gama tan amplia, es imposible utilizarlo directamente. Luego, debe usar HashCode y la longitud de la matriz para hacer una operación Y para obtener una posición que pueda aparecer en la matriz. Si dos elementos obtienen lo mismo , entonces se almacenan dos valores en esta matriz . Existen diferentes valores en la misma posición de la matriz y no se pueden sobrescribir La ventaja de la velocidad de insertar y eliminar la lista vinculada es más rápida, formando así una estructura de lista vinculada.-232~231 (-2147483648 ~ 2147483647)indexindex

De esta forma, la combinación de la matriz y la lista enlazada aumenta la velocidad de búsqueda, así como la velocidad de agregar y eliminar.

3. ¿Por qué se introdujo el árbol rojo-negro después de JDK8?

Primero, echemos un vistazo a la comparación de rendimiento entre la lista vinculada y el árbol rojo-negro, como se muestra a continuación:

  • Lista vinculada: complejidad de inserción O (1), complejidad de búsqueda O (n)
  • Árbol rojo-negro: complejidad de inserción O (logn), complejidad de búsqueda O (logn)
  • HashMapCuando el elemento de la matriz es una lista vinculada, la inserción utiliza directamente la inserción del encabezado y la complejidad de la inserción es O (1) ; cuando la lista vinculada es corta, no hay impacto en el rendimiento al buscar los datos. La lista es larga, la búsqueda afectará en gran medida el rendimiento.
  • En Java8, si la longitud de la matriz y la lista enlazada alcanza una cierta longitud, se convertirá en un árbol rojo-negro, lo que mejora el rendimiento de la búsqueda, pero cada vez que se insertan nuevos datos, la estructura de la red- se debe mantener el árbol negro y la complejidad es O (logn) . Esto puede considerarse como una compensación del rendimiento al buscar e insertar elementos, después de todo, se almacena para la búsqueda.

4. ¿Cuándo se convertirá la lista vinculada a un árbol rojo-negro?

Se dice que leer muchas publicaciones de blog tiene 链表la longitud alcanzada 8después de una, 链表se convertirá a 红黑树. De hecho, esta afirmación no es del todo correcta . Los candidatos a la admisión dicen que cuando 数组la longitud es mayor que 64, y 链表la longitud llega 8después de uno hasta la conversión a 红黑树. El código para convertir el árbol rojo-negro
HashMapen el putVal()medio (que se muestra a continuación), la
Inserte la descripción de la imagen aquí
mayoría de las personas pueden haber visto el código en el cuadro rojo de arriba y dijeron que cuando la longitud de la lista vinculada sea mayor que 8, convertirá el rojo. árbol negro. Entonces, estamos mirando treeifyBinel código del método. Como se muestra a continuación: Vemos a
Inserte la descripción de la imagen aquí
través treeifyBindel código fuente. Cuando la longitud de la matriz ( tab.length) es menor que MIN_TREEIFY_CAPACITY, resize()se llama al método para la expansión.

3. Capacidad de inicialización y factor de carga

Usamos HashMap, el uso habitual puede new HashMap();crear. En este caso, HashMapel tamaño predeterminado DEFAULT_INITIAL_CAPACITY = 1 << 4;es 16. Entonces, si la longitud pasada cuando la creamos es 17( es decir:) new HashMap(17);, HashMap¿cómo lidiar con ella?

3.1, encuentre el valor mínimo de la potencia de 2

En la inicialización de HashMap, existe tal método;

public HashMap(int initialCapacity, float loadFactor) {
    
    
        ...
        this.loadFactor = loadFactor; // 负载因子
        // 关键点:
        this.threshold = tableSizeFor(initialCapacity);
    }
  • El umbral threshold, tableSizeForcalculado por el método , se calcula de acuerdo con la inicialización.
  • Este método consiste en encontrar el valor más pequeño elevado a la n-ésima potencia de 2 que sea mayor que el valor inicial. Por ejemplo, si se pasa 17, el valor es 32.

Método de cálculo del tamaño del umbral;

    static final int tableSizeFor(int cap) {
    
    
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • MAXIMUM_CAPACITY = 1 << 30, este es el rango crítico, que es la colección de mapas más grande.
  • A primera vista, puede parecer un poco mareado. ¿Por qué se están desplazando todos 1, 2, 4, 8 y 16 hacia la derecha? Esto es principalmente para completar cada posición del binario con 1. Cuando cada posición del binario es 1, se convierte en estándar El múltiplo de 2 se resta por 1, y finalmente el resultado se incrementa en 1 y luego se devuelve.

Demostramos el número 17 de la siguiente manera:
Inserte la descripción de la imagen aquí

¿Por qué necesito alimentar el lado 2, verifique por qué HashMap valor inicial de la n-ésima potencia de 2?

3,2, factor de carga

static final float DEFAULT_LOAD_FACTOR = 0.75f;

Y el factor de carga está relacionado con la expansión, es decir, cuando HashMapel tiempo alcanza la cantidad de elementos en un determinado umbral, la necesidad actual de vaso de expansión.
Entonces, ¿por qué está configurado así? Como se mencionó anteriormente HashMap, los datos se almacenan internamente en la estructura de datos de una matriz más una lista enlazada o un árbol rojo-negro. Cuando almacenamos los datos, los hashhash en forma de cálculo del subíndice de la matriz por valor. puede haber más de uno en la misma posición de la matriz. Independientemente de si se trata de una lista vinculada o de un árbol rojo-negro, cuando el número de elementos en ella es grande, su búsqueda, inserción y eliminación se ralentizará. HashMapSu función es hash. Luego, puede aumentar el grado de hash a través de expansión para hacer la lista enlazada o roja El número de elementos en el árbol negro se reduce, lo que mejora el rendimiento.

  • Por lo tanto, es necesario elegir un tamaño razonable para la expansión.El valor predeterminado de 0,75 significa que cuando la capacidad de umbral ocupa 3 / 4s, expanda la capacidad rápidamente para reducir las colisiones Hash.
  • Al mismo tiempo, 0,75 es un valor de estructura predeterminado, que también se puede ajustar al crear un HashMap. Por ejemplo, si desea utilizar más espacio a cambio de tiempo, puede ajustar el factor de carga a un valor más pequeño para reducir las colisiones .

Cuatro reglas de cálculo de valor hash

Primero veamos el código fuente del valor HashMapcalculado hash, de la siguiente manera:

    static final int hash(Object key) {
    
    
        int h;
         // 计算hash 无符号右移 16位,是为了 高位参与运送
        // 减少 hash 冲突。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

Podemos ver que la parte clave es (h = key.hashCode()) ^ (h >>> 16). Mueva el valor hash a la derecha en 16 bits, que es exactamente la mitad de su longitud, y luego realice una operación XOR con el valor hash original, mezclando así los bits altos y bajos del valor hash original, aumentando la aleatoriedad .
Por ejemplo:
Por ejemplo, hay dos keyde hashCoderespectivamente 7C3B0000,1C3C0000 (hexadecimal), HashMaplongitud de la matriz 16. Estos dos son obviamente diferentes, pero después de tomar el módulo, ambos son 0 y hay un conflicto. Si también permite que los bits altos participen en el cálculo, los resultados serán diferentes, como se muestra a continuación:
Inserte la descripción de la imagen aquí
Como se puede ver en la figura anterior, los subíndices después de que los bits altos participen en el cálculo se han convertido en 11 y 12 respectivamente, reduciendo los conflictos. .

Cinco, por qué el hilo no es seguro

La inseguridad del hilo se refleja principalmente en los siguientes aspectos:

  1. En JDK7, cuando se amplía la capacidad, es fácil provocar un bucle sin fin.
  2. Causar pérdida de datos.
  3. HashMap obviamente tiene un valor, pero devuelve nulo cuando se obtiene.

Para obtener más información, consulte: ¿Por qué HashMap no es seguro para subprocesos?

Seis clases alternativas seguras para subprocesos

1. HashTable.
HashTableEs seguro para subprocesos Map, pero su interior es mediante synchronizedbloqueo de subprocesos mutex para lograrlo. El rendimiento es bajo.

2, Colecciones
uso Collectionsproporcionan synchronizedMappara construir un método de seguridad de clase hilo, que es también a través de los internos synchronizedhilos de bloqueo para lograr la exclusión mutua. El rendimiento es bajo.

3. ConcurrentHashMap
ConcurrentHashMaptambién se bloquea mediante bloqueo, pero se bloquea mediante bloqueo segmentado, y el rendimiento es mucho mayor que los dos anteriores.

Siete, la diferencia entre Java7 y Java8

Java7 Las Java8principales diferencias entre las versiones posteriores y las siguientes son:

  1. Estructura de datos: Java7 usa una estructura de datos de matriz + lista vinculada, mientras que Java 8 usa una matriz + lista vinculada y estructura de datos de árbol rojo-negro.
  2. Método de inserción: JDK1.7 usa el método de inserción de la cabeza, mientras que JDK1.8 y posteriores usan el método de inserción de la cola, entonces, ¿por qué hacen esto? Debido a que JDK1.7 es una extensión longitudinal con una lista enlazada individualmente, cuando se usa el método de inserción de la cabeza, es probable que ocurra el problema del orden inverso y el bucle sin fin de la lista enlazada circular. Pero después de JDK1.8, se debe a la adición del árbol rojo-negro para usar el método de interpolación de cola, lo que puede evitar el problema del orden inverso y el bucle sin fin de la lista vinculada.
  3. Después de la expansión, el método de cálculo de la ubicación de almacenamiento de datos es diferente. En JDK1.7, el valor hash y el número binario que deben expandirse se usan directamente para &. En JDK1.8, la regla de cálculo cuando JDK1.7 se usa directamente, es decir, la posición original antes de la expansión + el valor de expansión = el método de cálculo de JDK1.8, en lugar de la diferencia de JDK1.7. O el método . Pero este método es equivalente a simplemente juzgar si el bit recién agregado del valor hash involucrado en la operación es 0 o 1, y luego calcular directa y rápidamente el método de almacenamiento después de la expansión.

Referencia:
https://blog.csdn.net/qq_36520235/article/details/82417949
https://aobing.blog.csdn.net/article/details/103467732
https://bugstack.blog.csdn.net/article/ detalles / 107903915

Supongo que te gusta

Origin blog.csdn.net/small_love/article/details/112528723
Recomendado
Clasificación