HashMap
contiene una cierta cantidad de cubos. Se utiliza hashCode
para determinar en qué cubo colocarlos. En aras de la simplicidad, imagínelo como un módulo.
Si nuestro código hash es 123456 y tenemos 4 cubos, 123456 % 4 = 0
entonces el artículo va en el primer contenedor, Cubo 1.
Si nuestra función de código hash es buena, debería proporcionar una distribución uniforme para que todos los depósitos se utilicen de forma un tanto igual. En este caso, el depósito usa una lista vinculada para almacenar los valores.
Pero no puede confiar en que las personas implementen buenas funciones hash. La gente a menudo escribirá funciones hash deficientes que darán como resultado una distribución desigual. También es posible que tengamos mala suerte con nuestras aportaciones.
Cuanto menos uniforme sea esta distribución, más nos alejamos de las operaciones O (1) y más nos acercamos a las operaciones O (n).
La implementación de Hashmap intenta mitigar esto organizando algunos depósitos en árboles en lugar de listas vinculadas si los depósitos se vuelven demasiado grandes. Para eso TREEIFY_THRESHOLD = 8
es. Si un cubo contiene más de ocho elementos, debería convertirse en un árbol.
Este árbol es un árbol rojo-negro. Primero se ordena por código hash. Si los códigos hash son los mismos, utiliza el compareTo
método de Comparable
si los objetos implementan esa interfaz, de lo contrario, el código hash de identidad.
Si las entradas se eliminan del mapa, la cantidad de entradas en el depósito podría reducirse de modo que esta estructura de árbol ya no sea necesaria. Para eso UNTREEIFY_THRESHOLD = 6
es. Si la cantidad de elementos en un depósito cae por debajo de seis, también podríamos volver a usar una lista vinculada.
Finalmente, está el MIN_TREEIFY_CAPACITY = 64
.
Cuando un mapa hash aumenta de tamaño, se redimensiona automáticamente para tener más depósitos. Si tenemos un pequeño mapa hash, la probabilidad de que obtengamos depósitos muy llenos es bastante alta, porque no tenemos tantos depósitos diferentes para colocar cosas. Es mucho mejor tener un mapa hash más grande, con más depósitos y menos llenos. Esta constante básicamente dice que no debemos comenzar a convertir cubos en árboles si nuestro mapa hash es muy pequeño; primero debería cambiar el tamaño para ser más grande.
Para responder a su pregunta sobre la ganancia de rendimiento, estas optimizaciones se agregaron para mejorar el peor de los casos. Solo estoy especulando, pero probablemente solo vería una mejora notable en el rendimiento debido a estas optimizaciones si su hashCode
función no fuera muy buena.
String
, tienen un espacio de valor mucho mayor que elint
código hash, por lo que las colisiones son inevitables. Ahora depende de los valores reales, como los valores realesString
, que pones en el mapa, ya sea que obtengas una distribución uniforme o no. Una mala distribución puede ser el resultado de una mala suerte.java.lang.String
tiene un carácter determinista, no criptográficohashCode
, por lo que los atacantes pueden crear de manera trivial cadenas distintas con códigos hash en colisión. Antes de esta optimización, esto podía degradar las operaciones de HashMap a tiempo O (n), ahora simplemente las degrada a O (log (n)).if the objects implement that interface, else the identity hash code.
estaba buscando esta otra parte.MIN_TREEIFY_CAPACITY
. ¿Significa "Una vez que insertamos una clave que se va a aplicar hash al depósito que ya contiene 8TREEIFY_THRESHOLD
claves ( ) y si ya hay 64MIN_TREEIFY_CAPACITY
claves ( )HashMap
, la lista vinculada de ese depósito se convierte en un árbol equilibrado"?Para ponerlo más simple (tanto como pueda más simple) + algunos detalles más.
Estas propiedades dependen de muchas cosas internas que serían muy interesantes de entender, antes de pasar a ellas directamente.
TREEIFY_THRESHOLD -> cuando un solo cubo alcanza esto (y el número total excede
MIN_TREEIFY_CAPACITY
), se transforma en un nodo de árbol rojo / negro perfectamente equilibrado . ¿Por qué? Debido a la velocidad de búsqueda. Piense en ello de otra manera:Alguna introducción para el próximo tema. ¿Por qué la cantidad de contenedores / cubos es siempre una potencia de dos ? Al menos dos razones: más rápido que la operación de módulo y módulo en números negativos será negativo. Y no puede poner una Entrada en un depósito "negativo":
En cambio, se usa un buen truco en lugar de módulo:
Eso es semánticamente lo mismo que la operación de módulo. Mantendrá los bits inferiores. Esto tiene una consecuencia interesante cuando lo haces:
Aquí es donde entra en juego la multiplicación de cubos. Bajo ciertas condiciones (tomaría mucho tiempo explicarlo con detalles exactos ), los cubos se duplican en tamaño. ¿Por qué? Cuando los cubos se duplican en tamaño, entra en juego un bit más .
Como tal, este proceso se llama rehacer. Esto podría volverse lento. Eso es (para las personas que se preocupan) como HashMap se "bromea" como: rápido, rápido, rápido, lento . Hay otras implementaciones: buscar hashmap sin pausa ...
Ahora UNTREEIFY_THRESHOLD entra en juego después de volver a aplicar el hash. En ese punto, algunas entradas pueden moverse de estos contenedores a otros (agregan un bit más al
(n-1)&hash
cálculo y, como tal, pueden moverse a otros contenedores) y puede llegar a estoUNTREEIFY_THRESHOLD
. En este punto, no vale la pena mantener la papelera comored-black tree node
, sino como unLinkedList
lugar, comoMIN_TREEIFY_CAPACITY es la cantidad mínima de depósitos antes de que un determinado depósito se transforme en un árbol.
fuente
TreeNode
es una forma alternativa de almacenar las entradas que pertenecen a un solo contenedor deHashMap
. En implementaciones más antiguas, las entradas de un contenedor se almacenaban en una lista vinculada. En Java 8, si el número de entradas en un contenedor pasa un umbral (TREEIFY_THRESHOLD
), se almacenan en una estructura de árbol en lugar de la lista vinculada original. Esta es una optimización.Desde la implementación:
fuente
TREEIFY_THRESHOLD
Y el número total de contenedores es al menosMIN_TREEIFY_CAPACITY
. Intenté cubrir eso en mi respuesta ...Necesitaría visualizarlo: digamos que hay una clave de clase con solo la función hashCode () anulada para devolver siempre el mismo valor
y luego en otro lugar, estoy insertando 9 entradas en un HashMap con todas las claves siendo instancias de esta clase. p.ej
El recorrido del árbol es más rápido {O (log n)} que LinkedList {O (n)} y, a medida que n crece, la diferencia se vuelve más significativa.
fuente
compareTo
fromComparable
.identityHashCode
es otro mecanismo que utiliza.Key
no implementaComparable
,identityHashCode
se usará :)El cambio en la implementación de HashMap se agregó con JEP-180 . El propósito era:
Sin embargo, el rendimiento puro no es la única ventaja. También evitará el ataque HashDoS , en caso de que se use un mapa hash para almacenar la entrada del usuario, porque el árbol rojo-negro que se usa para almacenar datos en el depósito tiene la complejidad de inserción del peor de los casos en O (log n). El árbol se usa después de que se cumplen ciertos criterios; consulte la respuesta de Eugene .
fuente
Para comprender la implementación interna de hashmap, debe comprender el hash. El hash en su forma más simple, es una forma de asignar un código único para cualquier variable / objeto después de aplicar cualquier fórmula / algoritmo en sus propiedades.
Una verdadera función hash debe seguir esta regla:
“La función hash debe devolver el mismo código hash todas y cada una de las veces que se aplica la función en objetos iguales o iguales. En otras palabras, dos objetos iguales deben producir el mismo código hash de forma coherente ".
fuente