Principio subyacente de Java type-HashMap

Visión de conjunto

Otra URL

Principio de diseño de HashMap, estructura de datos de HashMap, implementación de código fuente de HashMap: aquí se omiten tres mil palabras -CSDN blog
6 estructura de datos-6.5 HashMap- "Notas técnicas de Simon" - 书 Stack 网 · BookStack

Comparación de JDK1.7 y JDK1.8

estructura de datos

Matrices y listas enlazadas

Hay matrices y listas enlazadas en la estructura de datos para almacenar datos, pero ambos tienen sus pros y sus contras.

Articulo Formación Lista enlazada
Huella de memoria Ocupa mucha memoria. (El intervalo de almacenamiento es continuo) Ocupa mucha memoria. (El intervalo de almacenamiento no es continuo)
Velocidad de búsqueda rápido. (La complejidad del tiempo es pequeña, O (1)) lento. (La complejidad del tiempo es muy grande, O (N))
Velocidad de inserción y eliminación lento. rápido.

Tabla de picadillo

Tabla hash: las características de las matrices integradas y las listas enlazadas : fácil de encontrar (dirección), fácil de insertar y eliminar, y una estructura de datos que ocupa un espacio medio .

Hay muchos métodos de implementación diferentes para tablas hash, y HashMap usa el método zipper, también conocido como [método de dirección de cadena].

 

La longitud inicial de la matriz de la tabla hash es 16 y cada elemento almacena el nodo principal de una lista vinculada. Luego, estos elementos se almacenan en la matriz de acuerdo con qué tipo de reglas. Generalmente se obtiene por % len hash (clave) , es decir, un valor hash de los elementos clave del módulo de longitud de matriz obtenido. Por ejemplo, en la tabla hash anterior:

12% 16 = 12,28% 16 = 12,108% 16 = 12,140% 16 = 12

Entonces, 12, 28, 108 y 140 se almacenan en la posición 12 de la matriz.

Mecanismo de acceso

Datos del par clave-valor

        Cada par clave-valor es un objeto Node <K, V>, que implementa la interfaz Map.Entry <K, V>. El nodo <K, V> tiene cuatro atributos: hash, clave, valor, siguiente (siguiente nodo).

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }
    // 其他代码
}

Dado que es una matriz lineal, ¿por qué se puede acceder a ella de forma aleatoria? Aquí HashMap usa un pequeño algoritmo, que se implementa aproximadamente de esta manera:

// 存储时:
int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值
int index = hash % Entry[].length;
Entry[index] = value;
 
// 取值时:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];

poner

Colisión hash

Si dos claves obtienen el mismo índice a través del hash% Entry []. Length, ¿cómo solucionarlo? La lista enlazada utilizada por HashMap. Hay un atributo siguiente en la clase Entrada, que apunta a la siguiente Entrada.

Por ejemplo,
         aparece el primer par clave-valor A, y el índice = 0 obtenido al calcular el hash de su clave se registra como: Entrada [0] = A.
         Después de un tiempo, aparece un par clave-valor B. Al calcular su índice también es igual a 0, HashMap hará lo siguiente: B.next = A, Entry [0] = B;
         si C vuelve a aparecer, y el índice es también igual a 0, luego C. siguiente = B, Entrada [0] = C; de
         esta manera, encontramos que el índice = 0 se usa realmente para acceder a los tres pares clave-valor de A, B y C usando el método de inserción de encabezado de la lista enlazada individualmente, y se enlazan entre sí mediante el siguiente atributo. Entonces habrá una situación de sobrescritura, y el último elemento insertado siempre se almacena en la matriz. Hasta ahora, deberíamos haber tenido claro la implementación aproximada de HashMap.

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value); //null总是放在数组的第一个链表中
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        //遍历链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果key在链表中已存在,则替换为新value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
 
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next
    //如果size超过threshold,则扩充table大小。再散列
    if (size++ >= threshold)
            resize(2 * table.length);
}

obtener

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    //先定位到数组元素,再遍历该元素处的链表
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

acceso de clave nula

La clave nula siempre se almacena en el primer elemento de la matriz Entry [].

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

Determinar la matriz

índice: hashcode% table.length 取 模

Al acceder a HashMap, es necesario calcular a qué elemento de la matriz Entry [] debe corresponder la clave actual, es decir, calcular el subíndice de la matriz; el algoritmo es el siguiente: 

// Returns index for hash code h.
static int indexFor(int h, int length) {
    return h & (length-1);
}

 La combinación bit a bit es equivalente a mod o resto%; esto significa que los subíndices de la matriz son los mismos, pero no significa que el hashCode sea el mismo.

tamaño de la mesa inicial

public HashMap(int initialCapacity, float loadFactor) {
    .....
    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity];
    init();
}

¡Tenga en cuenta que el tamaño inicial de la tabla no es la capacidad inicial en el constructor! !

¡Pero> = la n-ésima potencia de 2 de initialCapacity!

Colisión hash

Otra URL

Conflicto de hash y cuatro soluciones_Curiosity Big Bang-Blog de
CSDN_Conflicto de hash Una comprensión preliminar del conflicto de hash_ Nothing Mirror-Blog de CSDN de Xiaozhe_Hash Conflict
Estructura de datos y algoritmo: Hash Conflict Resolution-Know Almost

Introducción

 Hay cuatro formas de resolver los conflictos de hash

  • Direccionamiento abierto
  • Refrito
  • Dirección de cadena
  • Establecer un área de desbordamiento público

1. Método de direccionamiento abierto

Cuando la dirección hash p = H (clave) de la clave de la palabra clave entra en conflicto, se genera otra dirección hash p1 basada en p, si p1 todavía entra en conflicto, se genera otra dirección hash p2 basada en p, ... Hasta que una dirección hash no conflictiva pi se encuentra y el elemento correspondiente se almacena en él.

Hola = (H (tecla) + di)% m (i = 1,2 , n)

El método de direccionamiento abierto tiene los siguientes tres métodos:

  1. Detección lineal y hash
    1. Ver la siguiente unidad en orden hasta que se encuentre una unidad vacía o se busque en toda la tabla
    2. di = 1, 2, 3 ,… , m-1
  2. Detección y hash de dos (cuadrados)
    1. Detección de saltos a la izquierda y derecha de la tabla hasta que se encuentre una celda vacía o se busque en toda la tabla
    2. di = 1 ^ 2 , -1 ^ 2,2 ^ 2 , -2 ^ 2 ,… , k ^ 2 , -k ^ 2 (k <= m / 2)
  3. Detección y hash pseudoaleatorios
    1. Construya un generador de números pseudoaleatorios y dé un número aleatorio como punto de partida
    2. di = secuencia numérica pseudoaleatoria. En la implementación real, se debe establecer un generador de números pseudoaleatorios (como i = (i + p)% m), y se debe dar un número aleatorio como punto de partida.

      Por ejemplo, dada la longitud de la tabla hash m = 11, la función hash es: H (clave) = clave% 11, luego H (47) = 3, H (26) = 4, H (60) = 5, asumiendo que A es 69, entonces H (69) = 3, que entra en conflicto con 47.

      Si usa detección lineal y luego hash para tratar el conflicto, la siguiente dirección hash es H1 = (3 + 1)% 11 = 4, si todavía hay un conflicto, busque la siguiente dirección hash H2 = (3 + 2) % 11 = 5, todavía hay un conflicto, continúe buscando la siguiente dirección hash H3 = (3 + 3)% 11 = 6. En este momento, ya no hay conflicto, y 69 se completa en la unidad 5.

      Si usa detección secundaria y luego hash para tratar el conflicto, la siguiente dirección hash es H1 = (3 + 12)% 11 = 4, si todavía hay un conflicto, busque la siguiente dirección hash H2 = (3-12) % 11 = 2. No hay más conflicto en este momento, y 69 se completa en la Unidad 2.

      Si se usa la detección pseudoaleatoria y luego se aplica el hash para resolver el conflicto, y la secuencia de números pseudoaleatorios es: 2, 5, 9, ..., entonces la siguiente dirección hash es H1 = (3 + 2)% 11 = 5, aún en conflicto, Y busque la siguiente dirección hash H2 = (3 + 5)% 11 = 8. En este momento, no hay conflicto, y 69 se completa en la unidad 8.

ventaja

  1. Fácil de serializar
  2. Si se puede predecir el número total de datos, se puede crear una secuencia hash perfecta

Desventaja

  1. Ocupa mucho espacio. (Para reducir los conflictos, el método de direccionamiento abierto requiere que el factor de llenado α sea pequeño, por lo que se desperdiciará mucho espacio cuando el tamaño del nodo sea grande)
  2. Eliminar nodos es problemático. No puede simplemente establecer que el espacio del nodo eliminado esté vacío; de lo contrario, la ruta de búsqueda del nodo sinónimo que se completará en la tabla hash después de que se truncará. Esto se debe a que en varios métodos de dirección abierta, las unidades de dirección vacías (es decir, direcciones abiertas) son condiciones para el error de búsqueda. Por lo tanto, al realizar una operación de eliminación en una tabla hash que utiliza el método de dirección abierta para tratar los conflictos, solo puede marcar el nodo eliminado para eliminarlo, pero realmente no puede eliminar el nodo.

2. Re-hash

Proporcionar múltiples funciones hash. Si el valor hash de la clave calculado por la primera función hash entra en conflicto, la segunda función hash se utiliza para calcular el valor hash de la clave.

ventaja

  1. No es fácil de reunir

Desventaja

  1. Mayor tiempo de cálculo

3. Método de dirección en cadena

Para el mismo valor hash, use una lista vinculada para conectarse. ( HashMap usa este método )

ventaja

  1. Es sencillo lidiar con los conflictos y no hay acumulación. Es decir, los no sinónimos nunca entrarán en conflicto, por lo que la duración media de la búsqueda es más corta;
  2. Adecuado para situaciones en las que el número total cambia a menudo. (Porque el espacio de nodo en cada lista vinculada en el método de cremallera se aplica dinámicamente)
  3. Ocupa un pequeño espacio. El factor de relleno puede ser α≥1, y cuando el nodo es grande, el campo de puntero agregado en el método de cremallera se puede ignorar
  4. La operación de eliminar un nodo es fácil de implementar. Simplemente elimine el nodo correspondiente en la lista vinculada.

Desventaja

  1. La eficiencia de la consulta es baja. (El almacenamiento es dinámico, se necesita más tiempo para saltar al realizar consultas)
  2. Cuando se puede predecir el valor clave y no hay una operación de adición o modificación posterior, el rendimiento del método de direccionamiento abierto es mejor que el del método de direccionamiento en cadena.
  3. No es fácil de serializar

4. Establecer un área de desbordamiento público

La tabla hash se divide en dos partes: la tabla básica y la tabla de desbordamiento. Todos los elementos que entran en conflicto con la tabla básica se completan en la tabla de desbordamiento.

Mecanismo de expansión

Otra URL

Mecanismo de expansión de HashMap --- resize () _ estructura de datos y algoritmo_pan Blog de Jiannan-blog de CSDN
Mecanismo de expansión de HashMap-简 书
Mecanismo de expansión de HashMap ------ resize () _ blog de java_IM_MESSI-blog de
CSDN expansión de hashMap Mechanism_java_mengyue000's Blog -CSDN Blog

Cuando expandirse

        HashMap es una carga diferida. Después de construir el objeto HashMap, si no usa put para insertar elementos, HashMap no inicializará ni expandirá la tabla.

        Cuando se llama al método put por primera vez, HashMap encontrará que la tabla está vacía y luego llamará al método resize para inicializar.
        Cuando no se llama al método put por primera vez, si HashMap encuentra que el tamaño (tamaño de la matriz) es mayor que el umbral (el tamaño actual de la matriz multiplicado por el factor de carga), se llamará al método de cambio de tamaño para ampliar la capacidad.

        La matriz no se puede expandir automáticamente, por lo que solo se puede reemplazar con una matriz más grande para llenar los elementos anteriores y los nuevos elementos que se agregarán.

Descripción general de resize ()

  1. Determine si la capacidad de la matriz anterior antes de la expansión ha alcanzado el máximo (2 ^ 30)
    1. Si se alcanza, el umbral se modifica al valor máximo de Integer (2 ^ 31-1) y no habrá expansión en el futuro.
    2. Si no se alcanza, modifique el tamaño de la matriz a 2 veces el original
  2. Cree una nueva matriz con el nuevo tamaño de matriz (Nodo <K, V> [])
  3. Mueva los datos a la nueva matriz (Nodo <K, V> [])
    1. No necesariamente todos los nodos tienen que cambiar de posición.
      1. Por ejemplo, el tamaño de la matriz original es 16 y el tamaño es 32 después de la expansión. Si hay dos datos con valores hash de 1 y 17, su resto de 16 es 1 y están en el mismo depósito; después de la expansión, el resto de 1 a 32 sigue siendo 1, mientras que el resto de 17 a 32 se convierte en 17. Necesidad de cambiar de ubicación.
        1. El código correspondiente es: if ((e.hash & oldCap) == 0) Si es cierto, no es necesario cambiar la posición.
  4. Devuelve una nueva matriz Node <K, V> []
clase Capacidad inicial Maxima capacidad Multiplicador durante la expansión Factor de carga Implementación de bajo nivel
HashMap 2 ^ 4 2 ^ 30 n * 2 0,75 Entrada del mapa
HashSet 2 ^ 4 2 ^ 30 n * 2 0,75 HashMap <E, Objeto>
Tabla de picadillo 11 Entero.MAX_VALUE - 8 n * 2 + 1 0,75 Hashtable.Entry

        En HashMap, la longitud de la tabla de matriz de cubos hash debe ser 2 elevado a la enésima potencia (número no primo). Este es un diseño poco convencional. El diseño convencional consiste en diseñar el tamaño del cubo como un número primo. En términos relativos, la probabilidad de conflictos causados ​​por números primos es menor que la de los números no primos. Para obtener una prueba específica, consulte http://blog.csdn.net/liuqiyao_01/article/details/14475159. El tamaño del depósito inicial de Hashtable es 11, que es la aplicación en la que el tamaño del cubo está diseñado como número primo (no se puede garantizar que Hashtable sea un número primo después de la expansión).

         HashMap adopta este diseño poco convencional, principalmente para optimizar el módulo y la expansión, y para reducir los conflictos, cuando HashMap ubica la posición del índice del cubo de hash, también se suma al proceso de participación de alto nivel en la operación.

Código fuente

 HashMap # resize ()

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode) // 忽略这里的红黑树实现
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) { // 重点1:判断节点在resize之后是否需要改变在数组中的位置
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 重点2:将某节点中的链表分割重组为两个链表:一个需要改变位置,另一个不需要改变位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

Método transversal y su desempeño

URL de referencia

Java HashMap análisis de rendimiento transversal de tres bucles y ejemplos comparativos _java_ script alberga
iteradores Java (rpm) (Detallado, así como la diferencia entre iterador y para ciclismo) - redcoatjk - blog Park

Método transversal

método

Descripción / Ejemplo

para cada map.entrySet ()

Map <String, String> map = new HashMap <String, String> ();

para (Entrada <Cadena, Cadena> entrada: map.entrySet ()) {

  entry.getKey ();

  entry.getValue ();

}

Llame al iterador de conjunto de map.entrySet ()

Iterador <Map.Entry <String, String >> iterator = map.entrySet (). Iterator ();

while (iterator.hasNext ()) {

  entry.getKey ();

  entry.getValue ();

}

para cada map.keySet (), luego llame a get para obtener

Map <String, String> map = new HashMap <String, String> ();

para (clave de cadena: map.keySet ()) {

  map.get (clave);

}

Comparación de métodos transversales

Prueba de rendimiento y comparación de tres métodos transversales.

Entorno de prueba: sistema Windows7 de 32 bits, 3.2G, CPU de doble núcleo, memoria 4G, Java 7, Eclipse -Xms512m -Xmx512m

Resultados de la prueba:

Tamaño de mapa 10,000 100.000 1.000.000 2,000,000
para cada entrada 2ms 6ms 36 ms 91 ms
para iterador entrySet 0 ms 4 ms 35 ms 89 ms
para cada keySet 1 ms 6ms 48 ms 126 ms

Análisis de resultados del modo transversal (como se puede ver en la tabla anterior):

  • para cada entrySet y para el iterator entrySet son equivalentes en rendimiento
  • para cada keySet lleva mucho tiempo debido a la necesidad de llamar a get (key) para obtener el valor (si el algoritmo hash es deficiente, llevará más tiempo)
  • Si desea eliminar el mapa durante el bucle, solo puede usarlo para el iterador entrySet (introducido en HahsMap Non-Thread Safety).

Entrada de HashMap Establecer código fuente

private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
  public Map.Entry<K,V> next() {
    return nextEntry();
  }
}

HashMap keySet código fuente

private final class KeyIterator extends HashIterator<K> {
  public K next() {
    return nextEntry().getKey();
  }
}

Conocido por el código fuente:

keySet()与entrySet()都是返回set的迭代器。父类相同,只是返回值不同,因此性能差不多。只是keySet()多了一步根据key get value的操作而已。get的时间复杂度取决于for循环的次数,即hash算法。

public V get(Object key) {
  if (key == null)
    return getForNullKey();
  Entry<K,V> entry = getEntry(key);
  return null == entry ? null : entry.getValue();
}
/**
 1. Returns the entry associated with the specified key in the
 2. HashMap. Returns null if the HashMap contains no mapping
 3. for the key.
 */
final Entry<K,V> getEntry(Object key) {
  int hash = (key == null) ? 0 : hash(key);
  for (Entry<K,V> e = table[indexFor(hash, table.length)];
     e != null;
     e = e.next) {
    Object k;
    if (e.hash == hash &&
      ((k = e.key) == key || (key != null && key.equals(k))))
      return e;
  }
  return null;
}

 使用场景总结

方法

使用场景

for each map.entrySet()

循环中需要key、value,但不对map进行删除操作

调用map.entrySet()的集合迭代器

循环中需要key、value,且要对map进行删除操作

for each map.keySet()

循环中只需要key

hashCode方法

其他网址

Java String的hashcode()方法实现_timothytt的博客-CSDN博客

源码(String#hashCode)

String#hashCode 

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

为什么乘31呢?

选31是因为它是一个奇素(质)数,这里有两层意思:奇数 && 素数。

1.为什么是奇数,偶数不行?

    因为如果乘子是个偶数,并且当乘法溢出的时候(数太大,int装不下),相当于在做移位运算,有信息就损失了。

    比如说只给2bit空间,二进制的10,乘以2相当于左移1位,10(bin)<<1=00,1就损失了。

2.为什么是素数?

    作者说:你问我我问谁,这是传统吧。素数比较流弊。

    那么,问题又来了,那么多个奇素数,为什么就看上了31呢。

3.为什么偏偏是31?

    h*31 == (h<<5)-h; 现代虚拟机会自动做这样的优化,算得快。

    再反观这种“选美标准”下的其它数,

    h*7 == (h<<3)-h; // 太小了,容易hash冲突

    h*15 == (h<<4)-h; // 15不是素数

    h*31 == (h<<5)-h; // 31既是素数又不大不小刚刚好

    h*63 == (h<<6)-h; // 63不是素数

    h*127 == (h<<7)-h; // 太大了,乘不到几下就溢出了

实例追踪

"abc".hashCode()

hash为:0
value为:[a, b, c]

第一次循环:h = 0 + 97 = 97

第二次循环:h = 31 * 97 + 98 = 3105

第三次循环:h = 3105 * 31 + 99 = 96354

Supongo que te gusta

Origin blog.csdn.net/feiying0canglang/article/details/115184790
Recomendado
Clasificación