HashMap de la serie de código fuente

Introducción

  HashMap puede ser una de las colecciones más utilizadas por los programadores de Java, y las entrevistas también son una de las preguntas comunes de los exámenes. Entonces, ¿de dónde provienen las características que normalmente conocemos? ¿Qué es una lista vinculada por un tiempo y un árbol rojo-negro por un tiempo? Los concursantes de ensayos de estereotipos ya memorizaron el "Prefacio al Pabellón del Rey Teng" en la escuela secundaria y lo recitaron en sus cabezas como una reacción muscular. No es que esto no sea bueno, pero necesitamos saber por qué. A continuación, compartiré con ustedes el código fuente de HashMap basado en JDK 1.8. Puede que no sea adecuado para estudiantes que recién están comenzando a aprender HashMap y se requiere cierta base. El HashMap en JDK 1.8 tiene un total de 2425 líneas de código, incluidas 13 variables miembro, 51 métodos miembros y 14 clases internas. A continuación os lo contaré.

Algunas características de HashMap

  Antes de hablar oficialmente sobre el código fuente, revisemos algunas características estereotipadas de HashMap. Podemos mirar el código fuente con estas características y podemos tener una sensación como esta:

  • HashMap no admite la sincronización de subprocesos, lo que significa que no es seguro para subprocesos.
  • El valor clave de HashMap solo puede almacenar un valor nulo como máximo, y los valores pueden almacenar varios valores nulos.
  • HashMap está desordenado.
  • La capacidad de la matriz inicial predeterminada es 16.
  • La capacidad máxima de la matriz HashMap será solo 2 elevado a la enésima potencia.
  • Según el valor de capacidad inicial establecido por el usuario, se buscará hacia arriba la enésima potencia más pequeña de 2.
  • En JDK 1.8, se agregó recientemente una estructura de árbol rojo-negro. Cuando la longitud de la lista vinculada es mayor que 8 y la longitud de la matriz es mayor que 64, la lista vinculada se convertirá en un árbol rojo-negro para acelerar aumente la búsqueda, porque el rendimiento de búsqueda de la lista vinculada es O (n), el rendimiento de búsqueda de un árbol rojo-negro es O (log (n)).
  • Cuando la capacidad de HashMap alcance 0,75 veces la capacidad máxima de la matriz, se expandirá y la capacidad se duplicará, y luego los datos originales se copiarán a la nueva matriz. La expansión es una pérdida de tiempo. El tamaño de capacidad estimado , ¡reduciendo el número de tiempos de expansión!
  • Cuando se trata de conflictos de hash, al agregar valores de lista vinculada (no se ha utilizado el árbol rojo-negro), 1.7 es el método de inserción principal y 1.8 es el método de inserción final.
  • No hay diferencia en el uso de HashTable, excepto subprocesos inseguros y anulables.

Estructura de clases HashMap

El código fuente es el siguiente:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
      ...  }

El diagrama de estructura de clases es el siguiente:
Por favor agregue una descripción de la imagen.
  Podemos ver aquí que HashMap ha heredado AbstractMap y la clase AbstractMap ha implementado la interfaz Map, entonces, ¿por qué HashMap implementa la interfaz Map nuevamente? Y también hay tipos en LinkedList en ArrayList, ¿no es superfluo? Más tarde, tras comprobarlo, descubrí que, según la descripción del fundador Josh Bloch, esta forma de escribir era un error. En el marco de la colección Java, hay muchas formas de escribir como esta. Cuando escribió por primera vez el marco de la colección Java, pensó que escribir de esta manera podría ser valioso en algunos lugares, hasta que se dio cuenta de que estaba equivocado. Obviamente, los mantenedores del JDK no pensaron que valiera la pena modificar este pequeño error, por lo que se guardó así. De hecho, quedan muchos problemas históricos como este en JDK, algunos son por compatibilidad y otros no se pueden cambiar si creen que es innecesario, por lo que no debemos tener miedo del código fuente, es Todo escrito por personas y puede haber errores. ¿Cuál es la gran diferencia? ¿No puedo verlo una vez, no puedo verlo diez veces?
  Aquí entendemos que implementar nuevamente la interfaz Map es un error, y eso está bien. AbstractMap incluye principalmente operaciones comunes de la estructura de datos de tipo Mapa, mientras que Cloneable significa que HashMap se puede clonar y Serializable significa que HashMap se puede serializar.

Variables miembro de HashMap

  Aquí hay una introducción a las variables miembro de HashMap. Hay muchas características que podemos ver directamente desde las variables miembro.

  • static final long serialVersionUID = 362498820763181265L;
    el valor serialVersion utilizado durante la serialización.
  • static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    La capacidad inicial predeterminada de la tabla, es decir, la capacidad inicial predeterminada es 16 (debe ser 2 elevado a la enésima potencia).
  • static final int MAXIMUM_CAPACITY = 1 << 30;
    la capacidad máxima de la mesa, es decir, 2 elevado a 30.
  • flotación final estática DEFAULT_LOAD_FACTOR = 0,75f;
    factor de carga, utilizado para la expansión de capacidad, el valor predeterminado es 0,75.
  • static final int TREEIFY_THRESHOLD = 8;
    si la longitud de la lista vinculada es mayor que el umbral, se convertirá en un árbol rojo-negro.
  • static final int UNTREEIFY_THRESHOLD = 6;
    cuando el número de nodos en el árbol es menor o igual que este parámetro, se convertirá en una lista vinculada. Todos sabemos que en 1.8, después de que la lista vinculada se expande a 8 debido a colisiones hash, se convertirá en un árbol rojo-negro, entonces sabes que el árbol rojo-negro ¿Degenerará en una lista enlazada si es menor que 6?
  • static final int MIN_TREEIFY_CAPACITY = 64;
    La capacidad mínima de la tabla que el contenedor puede crear en árbol, es decir, la matriz HashMap es mayor que 64 y la lista de enlace único es mayor que 8, y el árbol rojo-negro se convertirá .
  • tabla transitoria Node<K,V>[];
    la matriz en el hash, aquí podemos ver que lo que se almacena en la matriz es en realidad el elemento Node, es decir, la clave y el valor completos, y el tamaño se basará en el valor inicial establecido por el usuario durante la inicialización. Redondear a un múltiplo entero de 2.
  • conjunto transitorio<Map.Entry<K,V>> conjunto de entrada;
    guardar conjunto de entrada en caché().
  • tamaño int transitorio;
    indica el número de pares clave-valor contenidos en el HashMap actual.
  • transient int modCount;
    Indica el número de modificaciones actuales de HashMap, que se utiliza principalmente para fallas rápidas de iteración (este valor se puede usar para juzgar si se realizan modificaciones concurrentes y si las modificaciones concurrentes arrojan directamente una excepción), por ejemplo, put() es +1 una vez, pero una determinada clave corresponde a El valor que se sobrescribe no es un cambio estructural.
  • umbral int;
    Indica el número máximo de pares clave-valor que el HashMap actual puede soportar. Una vez que se excede este número, el HashMap se expandirá, que es igual a la capacidad máxima actual * loadFactor.
  • float loadFactor;
    El factor de carga de la tabla hash. En realidad, el factor de carga puede ser mayor que 1, ¿lo sabías? La razón por la que no se establece un valor tan grande es porque la colisión de hash es demasiado grande, lo que afecta gravemente la eficiencia de la consulta. 0,75 es la mejor opción para equilibrar el espacio y el tiempo de búsqueda.
    Todas las variables miembro se muestran en la siguiente figura:
    Por favor agregue una descripción de la imagen.

Métodos miembros de HashMap

   Los siguientes son los métodos miembros de HashMap. Cada método se mencionará brevemente y los métodos importantes se explicarán en detalle.

  • public HashMap(int initialCapacity, float loadFactor);
    Constructor, usted mismo puede establecer la capacidad inicial y el factor de carga. Aquí, el factor de carga puede ser mayor que 1 o menor que 0,75. Puede elegir según la memoria y el tiempo de respuesta.

    public HashMap(int initialCapacity, float loadFactor) {
          
          
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
  • public HashMap (int initialCapacity);
    Constructor, puede establecer el valor de capacidad inicial usted mismo y el factor de carga es 0,75.

  • public HashMap();
    El constructor predeterminado, con una capacidad inicial de 16 y un factor de carga de 0,75.

  • public HashMap(Map<? extends K, ? extends V> m);
    Constructor, con una capacidad inicial de 16 y un factor de carga de 0,75, construye un nuevo HashMap con la misma estructura de mapeo que el Map especificado.

  • static final int hash(Object key);
    ​ hash es un valor hash + función de perturbación, que consiste en tomar primero el valor hashCode para la clave, luego perturbarlo y luego usarlo como valor hash, de modo que los bits altos puedan participe en la operación, reduzca las colisiones de hash y haga que los valores de los nodos se distribuyan de manera más uniforme, y muchas funciones utilizarán el método hash (clave de objeto) para calcular el valor hash.

static final int hash(Object key) {
    
    
    int h;
    // h = key.hashCode() 为第一步 取hashCode值
    // h ^ (h >>> 16)  为第二步 低16位与高16位做异或运算,使得之后在做&运算时,此时的低位实际上是高位与低位的结合,可大大增加随机性,减少了碰撞冲突的可能性
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Por ejemplo: cuando n es el valor predeterminado 16, tab[i = (n - 1) & hash] usa & para juzgar la posición donde debe caer el nodo, sin perturbaciones, si el cambio del bit alto del valor hash es grande Y el cambio del bit bajo es pequeño. Entonces muchos valores caerán en la misma posición y la colisión será muy grande.

  • Clase estática<?> comparableClassFor(Object x);
    ​ Si tiene el formato "Clase C implementa Comparable", devuelve la clase de x.
  • static int compareComparables(Class<?> kc, Object k, Object x);
    ​ Devuelve k.compareto(x) si x coincide con kc, en caso contrario 0.
  • static final int tableSizeFor(int cap);
    ​ Devuelve la enésima potencia del mínimo 2 que se toma hacia arriba para la capacidad objetivo dada, por ejemplo, si se da 7, será 8 si se toma, si se da 13, será 16 si se toma, calculado mediante la operación de bits inferida.
	static final int tableSizeFor(int cap) {
    
    
		// 先减一,是为了避免原本就是2的N次幂
        int n = cap - 1;
    	// 或操作,只要有一个1,结果则为1
        // 以下五步位运算操作,相当于把最高位到最后一位的数字全变成1
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        // 如果最后,n不小于0,且不大于等于MAXIMUM_CAPACITY,则返回n + 1,n + 1则是给定目标容量向上取的最小2的N次幂
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
    usa Map.putAll para crear un nuevo mapa de acuerdo con el mapa entrante, que es construido por public HashMap(Map<? extends K, ? extends V > m) El ejecutor real de la función.
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    
    
    int s = m.size();
    if (s > 0) {
    
    
      	// 如果当前数组为空,初始化阈值
        if (table == null) {
    
     // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 如果不为空,且大于阈值,则扩容
        else if (s > threshold)
            resize();
      	// 扩容后,遍历增加所有值即可
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
    
    
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}
  • public int size();
    devuelve la longitud de la colección Hash actual.
  • public boolean isEmpty()
    devuelve si la colección hash actual está vacía.
  • public V get(Object key);
    Devuelve el valor correspondiente a la clave en la colección actual. Si la clave no existe, devolverá nulo. Tenga en cuenta que devolver nulo no significa necesariamente que la clave no exista, o que el valor puede estar vacío.
  • Nodo final <K, V> getNode (int hash, clave de objeto); el
    ejecutor real del método get (clave de objeto) determina principalmente la posición en la matriz a través del valor hash calculado por la clave y luego se ejecuta en el árbol o Lista vinculada Busque, el siguiente código se usa en muchos lugares del código fuente, por lo que debe leerlo detenidamente:
// 传入key值算出的hash值,和key值
final Node<K,V> getNode(int hash, Object key) {
    
    
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 如果哈希数组不为空 且 长度大于0 且 hash值对应该的table[i]不为空,则进行查找,否则返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
    		// (n - 1) & hash 比单独取模快
        (first = tab[(n - 1) & hash]) != null) {
    
    
        // 如果对应table[i]第一个节点匹配,则直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果对应table[i]第一个节点不匹配,且next不为空,则继续匹配    
        if ((e = first.next) != null) {
    
    
        		// 如果之后的结构为树形结构(红黑树),则用树形的查找方式,如果不是树形结构,则用链表查询方式
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中进行元素匹配
            do {
    
    
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
  • El booleano público contiene clave (clave de objeto)
    determina si se incluye el valor de clave dado. Llame a getNode(int hash, clave de objeto) directamente y devuelva verdadero si el valor no es nulo.
  • La operación pública V put (clave K, valor V)
    para agregar un valor llama directamente al método putVal (…), que se puede decir que es uno de los núcleos de todo HashMap, que se discutirá a continuación.
  • final V putVal (int hash, clave K, valor V, boolean onlyIfAbsent, boolean evict);​ La
    implementación específica de put (clave K, valor V), el análisis del código es el siguiente:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果tab没有初始化,则进行扩容初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果对应tab[i]为空,没有发生碰撞,则直接加入新节点    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 发生碰撞进行如下操作    
        else {
    
    
            Node<K,V> e; K k;
            // 如果第一个节点匹配,则覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果不匹配,且为红黑树,则用红黑树的putTreeVal(...)方法操作
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 如果不匹配,且为链表,则用链表的方式解决
            else {
    
    
            		// 在链表中查找给定节点
                for (int binCount = 0; ; ++binCount) {
    
    
                		// 如果未查到旧值,则新加入一个节点,采用尾插法
                    if ((e = p.next) == null) {
    
    
                        p.next = newNode(hash, key, value, null);
                        // 如果尾插后大于 TREEIFY_THRESHOLD ,则转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果找到旧值,退出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 新值覆盖旧值,且返回旧值
            if (e != null) {
    
     // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 修改次数加1
        ++modCount;
        // size加1,如果超过threshold,则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

Todo el proceso es como se muestra en la siguiente figura:
inserte la descripción de la imagen aquí

  • final Node<K,V>[] resize()
    ​ La expansión resize() es recalcular y expandir la capacidad. Cuando se agregan elementos continuamente al objeto HashMap y la matriz dentro del objeto HashMap no puede cargar más elementos, el objeto Es necesario ampliar la longitud de la matriz para que se puedan acomodar más elementos. Por supuesto, la matriz en Java no se puede expandir automáticamente. El método consiste en crear una nueva matriz y "copiar" todos los valores originales a la nueva matriz. Copiar es una operación que requiere mucho tiempo, por lo que deberíamos intentarlo. nuestro mejor esfuerzo al inicializar. Posibilidad de especificar el tamaño de la matriz para reducir el comportamiento de copia.

​ El código se analiza de la siguiente manera:

		final Node<K,V>[] resize() {
    
    
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果不是第一次初始化,新容量为原来的两倍(<< 1)
        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);
        }
        // 如果newThr为0,则我们重新计算该值等于newCap * loadFactor;
        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) {
    
    
        		// 把每个bucket都移动到新的buckets中
            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
                    		// 链表优化重hash的代码块
                        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) {
    
    
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引 + oldCap
                            // 1.8之后的扩容为原来的两部,无需重新计算hash值,可直接计算位置
                            else {
    
    
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket里
                        if (loTail != null) {
    
    
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
    
    
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • final void treeifyBin(Node<K,V>[] tab, int hash)
    Reemplaza todos los nodos vinculados en el contenedor bajo el hash dado, a menos que la tabla sea demasiado pequeña, en cuyo caso cambia el tamaño.
final void treeifyBin(Node<K,V>[] tab, int hash) {
    
    
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    
    
        TreeNode<K,V> hd = null, tl = null;
        do {
    
    
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
    
    
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
  • public void putAll(Map<? extends K, ? extends V> m);
    agregue todos los valores en el mapa y llame directamente al método putMapEntries(Map<? extends K, ? extends V> m, boolean evict).
  • public V remove(Object key);
    ​ Para eliminar un nodo, llame al método removeNode() directamente.
  • Nodo final <K, V> removeNode (int hash, clave de objeto, valor de objeto, boolean matchValue, boolean movable)
    operación de eliminación de nodo, similar al método getNode (int hash, clave de objeto), busque primero el nodo especificado, si es un estructura de árbol, luego use la operación de eliminación en forma de árbol, y si es una estructura de lista vinculada, use el método de eliminación de lista vinculada.
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 &&
        (p = tab[index = (n - 1) & hash]) != null) {
    
    
        Node<K,V> node = null, e; K k; V v;
      	// 首先查找到该节点,和getNode(int hash, Object key)方法类似
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
    
    
          	// 如果是红黑树,则用树的查找方式,如果是链表,则直接遍历查找到为止
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
    
    
                do {
    
    
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
    
    
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
      	// 如果找到该节点,则进行删除操作,并返回该节点值
        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;
          	// 修改次数 + 1
            ++modCount;
          	// 尺寸 - 1
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
  • public void clear()
    ​ Elimina todos los elementos de la colección.
	public void clear() {
    
    
        Node<K,V>[] tab;
        // 修改次数加1
        modCount++;
        if ((tab = table) != null && size > 0) {
    
    
            // 尺寸归0
            size = 0;
            // 释放所有链表或者红黑树,这些对象会在垃圾回收中被回收
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }
  • booleano público contieneValue (valor del objeto);
    descubre si hay un valor determinado en la colección Valor.
  • public Set<K> keySet();
    devuelve el conjunto de todas las claves.
  • public Collection<V>values()
    devuelve una colección de todos los valores.
  • public Set<Map.Entry<K,V>> EntrySet()
    ​ Devuelve una colección de todas las entradas, que incluye la clave y el valor correspondiente.
  • public V getOrDefault(Object key, V defaultValue);
    ​ Encuentre el valor correspondiente a la clave, si no existe, devuelva el valor predeterminado, que es el defaultValue pasado.
  • public V putIfAbsent(K key, V value);
    Si el valor no existe, se almacena y se llama directamente putVal(...).
  • public boolean remove(Clave de objeto, Valor de objeto);
    ​ Para eliminar un nodo, llame a removeNode(...) directamente.
  • reemplazo booleano público (clave K, V oldValue, V newValue);
    para reemplazar un nodo, de hecho, después de que getNode(...) encuentra el nodo, devuelve verdadero si el reemplazo es exitoso y falso si falla.
  • reemplazo público de V (clave K, valor V)
    ​ Para reemplazar un nodo, de hecho, después de que getNode(…) encuentra el nodo, devuelve el valor del nodo si el reemplazo es exitoso y devuelve nulo si falla.
  • public V ComputeIfAbsent (tecla K, función <? Super K,? Extiende V> MappingFunction);
    si el nodo dado no existe, use MappingFunction para generar un nuevo valor de nodo.
  • public V ComputeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
    Si el nodo dado existe, use remappingFunction para calcular y generar un nuevo valor.
  • El cálculo público de V (tecla K, BiFunction<? super K, ? super V, ? extiende V> remappingFunction)
    calcula con el valor de nodo dado y la función de remapping, y genera un nuevo valor. Cuando el valor de nodo dado no existe, use el valor nulo valor para el cálculo.
  • fusión pública de V (tecla K, valor V, BiFunction <? super V, ? super V, ? extiende V> remappingFunction);
    utilice la función remappingFunction para comparar y fusionar el valor correspondiente a la clave con el valor del parámetro.
  • public void forEach(BiConsumer<? super K, ? super V> action)
    es una operación transversal que puede aceptar una acción para operar sobre los elementos de la colección.
  • public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
    operación de reemplazo, que puede aceptar una función para operar en los elementos de la colección.
  • public Object clone();
    Clonar un HashMap y regresar
  	public Object clone() {
    
    
      HashMap<K,V> result;
      try {
    
    
          result = (HashMap<K,V>)super.clone();
      } catch (CloneNotSupportedException e) {
    
    
          // this shouldn't happen, since we are Cloneable
          throw new InternalError(e);
      }
      // 初始化一些信息
      result.reinitialize();
      // 加入当前集合所有元素
      result.putMapEntries(this, false);
      return result;
  }
  • final float loadFactor();
    ​ Devuelve el valor del factor de carga.
  • final int capacidad()
    devuelve el valor de capacidad actual.
  • private void writeObject(java.io.ObjectOutputStream s);
    ​ Guarde el HashMap en una secuencia y serialícelo.
  • private void readObject(ObjectInputStream s)
    lee el HashMap de la secuencia y lo deserializa.
  • Nodo<K,V> nuevoNodo(int hash, clave K, valor V, Nodo<K,V> siguiente)
    ​ Crea un nodo normal (que no sea de árbol).
  • Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next);
    ​ Se utiliza para convertir de TreeNodes a nodos normales.
  • TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next)
    crea un nodo de árbol.
  • TreeNode<K,V> reemplazoTreeNode(Node<K,V> p, Node<K,V> next); se
    utiliza para convertir de TreeNodes a nodos ordinarios.
  • void reinitialize()
    ​ Restablecer el estado predeterminado inicial. Llamado por clon y readObject.
  • void afterNodeAccess(Node<K,V> p);
    Permitir devolución de llamada posterior a la acción de LinkedHashMap.
  • void afterNodeInsertion(boolean desalojo)
    ​ Permitir devolución de llamada posterior a la acción de LinkedHashMap.
  • void afterNodeRemoval(Node<K,V> p);
    Permitir devolución de llamada posterior a la acción de LinkedHashMap.
  • void internalWriteEntries(java.io.ObjectOutputStream s);
    ​ Se llama solo desde writeObject para garantizar un orden compatible.
    Todos los métodos se muestran en la siguiente figura.
    Por favor agregue una descripción de la imagen.
    Por favor agregue una descripción de la imagen.

Clase interna de HashMap

  La siguiente es la clase interna de HashMap.

  • La clase estática Node<K,V> implementa Map.Entry<K,V>{…}
    El nodo HashMap más básico, donde existe el valor de un solo nodo.

    static class Node<K,V> implements Map.Entry<K,V> {
          
          
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    
        Node(int hash, K key, V value, Node<K,V> next) {
          
          
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    
        public final K getKey()        {
          
           return key; }
        public final V getValue()      {
          
           return value; }
        public final String toString() {
          
           return key + "=" + value; }
    
        public final int hashCode() {
          
          
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
    
        public final V setValue(V newValue) {
          
          
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
    
        public final boolean equals(Object o) {
          
          
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
          
          
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
    
  • La clase final KeySet extiende AbstractSet<K>{…}
    hereda la clase abstracta AbstractSet y se utiliza para almacenar valores clave en la colección.

  • final class Values ​​​​extend AbstractCollection<V>{…}
    hereda la clase abstracta AbstractSet, que se utiliza para almacenar los valores en la colección.

  • La clase final EntrySet extiende AbstractSet<Map.Entry<K,V>>{…}
    hereda la clase abstracta AbstractSet y se utiliza para almacenar la colección de entradas en la colección.

  • La clase final estática privada UnsafeHolder{…}
    admite el restablecimiento de los campos finales durante la deserialización.

  • clase abstracta HashIterator{…}
    Iterador general en HashMap.

    abstract class HashIterator {
          
          
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
    
        HashIterator() {
          
          
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) {
          
           // advance to first entry
                do {
          
          } while (index < t.length && (next = t[index++]) == null);
            }
        }
    
        public final boolean hasNext() {
          
          
            return next != null;
        }
    
        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;
        }
    
        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;
        }
    }
    
  • clase final KeyIterator extiende HashIterator implementa Iterator<K> {…}
    Heredado de HashIterator, iterador para clave.

  • clase final ValueIterator extiende HashIterator implementa Iterator<V>{…}
    Heredado de HashIterator, iterador de valor.

  • clase final EntryIterator extiende HashIterator implementa Iterator<Map.Entry<K,V>>{…}
    Heredado de HashIterator, iterador para entrada.

  • clase estática HashMapSpliterator<K,V>{…}
    Un iterador dividido de propósito general en HashMap, una nueva función agregada en jdk 1.8, para atravesar elementos en paralelo.

    static class HashMapSpliterator<K,V> {
          
          
        final HashMap<K,V> map;
        Node<K,V> current;          // current node
        int index;                  // current index, modified on advance/split
        int fence;                  // one past last index
        int est;                    // size estimate
        int expectedModCount;       // for comodification checks
    
        HashMapSpliterator(HashMap<K,V> m, int origin,
                           int fence, int est,
                           int expectedModCount) {
          
          
            this.map = m;
            this.index = origin;
            this.fence = fence;
            this.est = est;
            this.expectedModCount = expectedModCount;
        }
    
        final int getFence() {
          
           // initialize fence and size on first use
            int hi;
            if ((hi = fence) < 0) {
          
          
                HashMap<K,V> m = map;
                est = m.size;
                expectedModCount = m.modCount;
                Node<K,V>[] tab = m.table;
                hi = fence = (tab == null) ? 0 : tab.length;
            }
            return hi;
        }
    
        public final long estimateSize() {
          
          
            getFence(); // force init
            return (long) est;
        }
    }
    
  • clase final estática KeySpliterator<K,V> extiende HashMapSpliterator<K,V> implementa Spliterator<K>{…}
    Heredado de HashMapSpliterator, iterador divisible para clave.

  • clase final estática ValueSpliterator<K,V> extiende HashMapSpliterator<K,V> implementa Spliterator<V>{…}
    Heredado de HashMapSpliterator, usado para iterador de valor divisible.

  • La clase final estática EntrySpliterator<K,V> extiende HashMapSpliterator<K,V> implementa Spliterator<V>{…}
    hereda de HashMapSpliterator y se usa como un iterador dividido para la entrada.

  • clase final estática TreeNode<K,V> extiende LinkedHashMap.Entry<K,V>{…} La
    estructura de árbol del árbol rojo-negro es relativamente complicada de operar, y escribiré un artículo sobre el árbol rojo-negro más adelante. que no será discutido por el momento.

    		static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
          
          
    				// 父节点
            TreeNode<K,V> parent;  // red-black tree links
            // 左子树
            TreeNode<K,V> left;
            // 右子树
            TreeNode<K,V> right;
            // 前一节点
            TreeNode<K,V> prev;    // needed to unlink next upon deletion
            // 是否为红色节点
            boolean red;
            
            TreeNode(int hash, K key, V val, Node<K,V> next) {
          
          
                super(hash, key, val, next);
            }
    
            // 返回根节点
            final TreeNode<K,V> root() {
          
          
                ...
            }
    
            // 确保给定的根节点是其bin的第一个节点
            static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
          
          
                ...
            }
    
            // 查找节点
            final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
          
          
                ...
            }
    
            // 调用根查找节点
            final TreeNode<K,V> getTreeNode(int h, Object k) {
          
          
                ...
            }
    
            
            static int tieBreakOrder(Object a, Object b) {
          
          
                ...
            }
    
            // 形成从该节点链接的节点树
            final void treeify(Node<K,V>[] tab) {
          
          
                ...
            }
    
            // 把树节点转化成链表节点
            final Node<K,V> untreeify(HashMap<K,V> map) {
          
          
                ...
            }
    
            // 向树中加入新值
            final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                           int h, K k, V v) {
          
          
                ...
            }
    
            // 从树中删除值
            final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                      boolean movable) {
          
          
                ...
            }
    
            // 将树分裂成两颗树
            final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
          
          
                ...
            }
    
            /* ------------------------------------------------------------ */
            // Red-black tree methods, all adapted from CLR
    				
    				// 左旋
            static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                                  TreeNode<K,V> p) {
          
          
                ...
            }
    
    				// 右旋
            static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                                   TreeNode<K,V> p) {
          
          
                ...
            }
    
            static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                        TreeNode<K,V> x) {
          
          
                ...
            }
    
            static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
                                                       TreeNode<K,V> x) {
          
          
                ...
            }
    
            // 递归检查不变量
            static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
          
          
                ...
            }
        }
    

Todas las clases internas se muestran en la siguiente figura:
Por favor agregue una descripción de la imagen.

Implementación de varios mecanismos de HashMap.

  Además de la introducción del método que acabamos de presentar, creo que hay algunos mecanismos importantes que deben destacarse; consulte los detalles a continuación.

Inicializar capacidad

   La inicialización buscará la potencia más pequeña de 2 según el valor de capacidad inicial establecido por el usuario. Para obtener más detalles, consulte la explicación del método tableSizeFor (int cap) anterior. La razón por la que la capacidad se toma como la enésima potencia de 2 es principalmente por conveniencia para cálculos de módulo posteriores.

método de función de perturbación hash

  El objetivo principal de la función de perturbación es hacer que el valor hash original sea más aleatorio, de modo que los nodos puedan distribuirse más uniformemente en la matriz. Ya lo hemos explicado claramente en el método hash (clave de objeto) , así que no lo haré. entra en detalles aquí .

Encuentre la posición del índice del depósito de hash

   Para encontrar la posición del índice del depósito de hash en HashMap, hay tres pasos en total:

  • Obtener el valor hashCode de la clave
  • Operación XOR de bits bajos y altos
  • operación de módulo

  Los dos primeros pasos se han implementado en hash (clave de objeto), y el proceso también se explica claramente. El enfoque aquí está en la operación del módulo. Sin embargo, el consumo de la operación del módulo directo es relativamente grande. Se utiliza un método muy inteligente en HashMap, ahora mismo:

h & (table.length -1)	

   La longitud de la matriz subyacente de HashMap es siempre 2 elevado a la enésima potencia, que es la optimización de la velocidad de HashMap. Cuando la longitud es siempre 2 elevado a la enésima potencia, la operación h& (longitud-1) es equivalente a la longitud del módulo, es decir, h%longitud, pero & es más eficiente que %. Aquí, después de calcular el índice i tomando el módulo, use directamente la tabla [i] para encontrar la posición correspondiente al valor hash.

expansión

  La expansión consiste en recalcular y expandir la capacidad. Cuando se agregan continuamente elementos al objeto HashMap y la matriz dentro del objeto HashMap no puede contener más elementos, el objeto necesita expandir la longitud de la matriz para que se puedan cargar más elementos. Por supuesto, la matriz en Java no se puede expandir automáticamente. El método consiste en crear una nueva matriz y "copiar" todos los valores originales a la nueva matriz. Copiar es una operación que requiere mucho tiempo, por lo que deberíamos intentarlo. hacemos lo mejor que podemos al inicializar. Es posible especificar el tamaño de la matriz para reducir el comportamiento de copia. Para obtener más detalles, consulte la explicación detallada de resize().

hilo inseguro

  En JDK 1.7, las operaciones de subprocesos múltiples pueden causar un problema de bucle infinito. En JDK 1.8, no lo hará, pero sigue siendo una operación insegura para subprocesos. No se recomienda su uso. ConcurrentHashMap se puede usar para concurrencia de subprocesos múltiples Escribiré un artículo sobre Espere con ansias el análisis del código fuente de ConcurrentHashMap.

La diferencia entre jdk versión 1.7 y jdk versión 1.8

  • 1.7 La estructura de datos subyacente es: matriz + lista vinculada. 1.8 La estructura de datos subyacente es: matriz + lista vinculada + estructura de árbol rojo-negro (cuando la longitud de la lista vinculada es mayor que 8 y la matriz es mayor que 64, se convertirá en un árbol rojo-negro).
  • Los nodos recién agregados en 1.7 son el método de inserción de cabeza y los nuevos nodos en 1.8 son el método de inserción de cola. El método de inserción de cola también es la razón por la cual la lista de anillos no es fácil de aparecer en 1.8.
  • 1.7 El procesamiento de perturbaciones se realiza 9 veces = operaciones de 4 bits + 5 operaciones XOR, 1.8 El procesamiento de perturbaciones se realiza 2 veces = operación de 1 bit + 1 operación XOR.
  • 1.7 es una expansión antes de insertar datos y 1.8 es una expansión después de insertar datos correctamente.
  • 1.7 El cálculo de la posición después de la expansión es: consistente con el original, es decir: hashCode -> perturbación -> módulo. 1.8 Para ser más eficiente, se calcula de acuerdo con la ley después de la expansión de capacidad: posición después de la expansión de capacidad = posición original o posición original + capacidad anterior.

La diferencia con HashTable

  • Por el contrario, HashTable es seguro para subprocesos y no permite que la clave y el valor sean nulos.
  • La capacidad predeterminada de HashTable es 11.
  • HashTable usa directamente el código hash (key.hashCode()) de la clave como valor hash, a diferencia de HashMap que usa la función de perturbación estática final int hash (clave de objeto) para perturbar el código hash de la clave como valor hash.
  • HashTable toma el subíndice del depósito de hash directamente usando la operación de módulo% (porque su capacidad predeterminada no es la enésima potencia de 2. Por lo tanto, es imposible reemplazar la operación de módulo con una operación de bits), y la operación de bits utilizada por HashMap será Hay muchos más rápidos que la simple operación de módulo, básicamente un golpe de reducción de dimensionalidad.
  • Al expandirse, la nueva capacidad es 2 veces +1 de la capacidad original. int nuevaCapacidad = (antiguaCapacidad << 1) + 1 .
  • Hashtable es una subclase de Dictionary y también implementa la interfaz Map. HashMap es una clase de implementación de la interfaz Map.
    Aunque HashTable es seguro para subprocesos, básicamente no se usa ahora debido al bajo rendimiento. Si se requieren operaciones seguras para subprocesos, podemos usar ConcurrentHashMap.

Referencias

Volver a comprender HashMap de la serie Java 8

Supongo que te gusta

Origin blog.csdn.net/qq_22136439/article/details/128434837
Recomendado
Clasificación