(Java) Notas --- Análisis de los principios subyacentes de HashMap y las preguntas de la entrevista de HashMap

contenido

1. Interfaz implementada

2. Valor inicial predeterminado

1. Capacidad inicial predeterminada

2. Capacidad máxima predeterminada

3. Factor de carga predeterminado

3. Interconversión entre lista enlazada y árbol rojo-negro

4. La estructura de la lista enlazada en el cubo hash

5. Función hash

6. Expansión

7. Métodos comúnmente utilizados en HashMap

1. Constructor

2. Encuentra, obtén el valor según la clave

3. Comprobar si existe la clave 

4. Insertar

5. Eliminar

8. Preguntas frecuentes sobre HashMap


1. Interfaz implementada

La capa inferior implementa interfaces de mapa, clonación y serialización

2. Valor inicial predeterminado

1. Capacidad inicial predeterminada

2^4 = 16, cuando no se proporciona la capacidad inicial, la capacidad predeterminada es 16

2. Capacidad máxima predeterminada

La capacidad máxima predeterminada es 2^30 

3. Factor de carga predeterminado

El factor de carga predeterminado es 0,75, el número de elementos efectivos / capacidad de la mesa = factor de carga

3. Interconversión entre lista enlazada y árbol rojo-negro

El depósito hash almacena los nodos de la lista vinculada, pero bajo ciertas condiciones, la lista vinculada y el árbol rojo-negro se convertirán entre sí.

Si el número de nodos de la lista vinculada en cada grupo supera los 8, la lista vinculada se convertirá en un árbol rojo y negro.

Cuando el número de nodos en el árbol rojo-negro es inferior a 6, el árbol rojo-negro degenerará en una lista enlazada

Si el número de nodos en una lista vinculada en el cubo hash supera los 8 y el número de cubos supera los 64, la lista vinculada se convertirá en un árbol rojo-negro; de lo contrario, la capacidad se expandirá directamente 

4. La estructura de la lista enlazada en el cubo hash

//HashMap将其底层链表中的节点封装为静态内部类
//节点中带有key,value键值对以及key所对应的哈希值
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; }
        
        //重写Object类中hashcode()方法
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        
        //重写Object类中equals方法
        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;
        }
    }

5. Función hash

Convierta la clave en un número entero y luego use este número para dividir el método restante para calcular la posición del cubo 

Analizar gramaticalmente:

1. Si la clave es nula, devolver el cubo 0.

2. Si la clave no es nula, se devuelve el código hash correspondiente a la clave.Si la clave es de un tipo personalizado, se debe reescribir el método hashcode() en la clase Object.

3. ( h = clave . hashCode() ) ^ ( h >>> 16 ), para mantener los 16 bits altos sin cambios, los 16 bits bajos y los 16 bits altos son XORed, principalmente usados ​​cuando la matriz hashmap es relativamente pequeño, todos los bits participan en la operación, el propósito es reducir la colisión.

4. Después de obtener la dirección hash, calcule el número de depósito de la siguiente manera: index = (table.length - 1) & hash.

5. El número de cubo se obtiene dividiendo el método del resto, porque el tamaño de la tabla hash es siempre la n-ésima potencia de 2 , por lo que el módulo se puede convertir en una operación de bits para mejorar la eficiencia, que es una de las razones por las que el la capacidad debe ampliarse 2 veces.

Aquí hay una imagen para ilustrar por qué:

Resumen: del método anterior, se puede ver que muchos bits del código hash en realidad no se usan, por lo que la operación de cambio se usa en la función hash del hashMap, y solo los primeros 16 bits se toman para el mapeo. Mano, la relación de operación y el modelo que toma la eficiencia es mayor. 

6. Expansión

Cada vez que cap se expande a la n-ésima potencia de 2 más cercana a cap , int n = cap - 1, para evitar que cap sea una potencia de 2, si cap ya es una potencia de 2, la ejecución se completa. pocas operaciones de desplazamiento a la derecha sin firmar, la capacidad devuelta será el doble del límite.

Suponiendo que el valor inicial de cap ahora es 10, el método específico es el siguiente:

7. Métodos comúnmente utilizados en HashMap

1. Constructor

 // 构造方法一:带有初始容量的构造,负载因子使用默认值0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    // 构造方法二:
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    // 构造方法一:带有初始容量和初始负载因子的构造
    public HashMap(int initialCapacity, float loadFactor) {
        // 如果容量小于0,抛出非法参数异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        // 如果初始容量大于最大值,用2^30代替
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

        // 检测负载因子是否非法,如果负载因子小于0,或者负载因子不是浮点数,抛出非法参数异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        // 给负载因子和容量赋值,并将容量提升到2的整数次幂
        // 注意:构造函数中并没有给
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
/*
注意:
不同于Java7中的构造方法,Java8对于数组table的初始化,并没有直接放在构造器中完成,而是将table数组的构
造延迟到了resize中完成
*/

2. Encuentra, obtén el valor según la clave

/*
     1. 通过key计算出其哈希地址,然后借助哈希地址在哈希桶中找到与key对应的节点
     2. 如果节点为null,返回null,说明HashMap中节点是可以为空的
     3. 如果节点不为空,返回该节点中的value
    */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 1. 先检测哈希桶是否为空
        // 2. 检测哈希桶的个数是否大于零,如果桶不空,桶的个数肯定不为0
        // 3. n-1&hash-->计算桶号
        // 4. 当前桶是否为空桶
        // 如果1 2 3 4均不成立,说明当前桶中有节点,拿到当前桶中第一个节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {

            // 如果节点的哈希值与key的哈希值相等,然后再检测key是否相等
            // 如果相等,则返回该节点
            // 此处也进一步证明了:HashMap必须要重写hashCode和equals方法
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 如果第一个节点后还有节点,检测first是否为treeNode类型的
            // 因为如果哈希桶中某条链节点大于8个,为了提高性能,HashMap会将链表替换为红黑树
            // 此时再红黑树中找与key对应的节点
            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;
    }

3. Comprobar si existe la clave 

    /*
1. 先通过getNode()获取与key对应的节点
2. 如果节点不为空,说明存在返回true,否则返回false
3. 时间复杂度:平均为O(1),如果当天key所对应的桶中挂接的链表则顺序查找,挂接的是红黑树按照红黑树性质找
*/
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

4. Insertar

    /*
1. 先使用key借助hash函数计算key的哈希地址
2. 将key-value键值对,结合计算出的hash地址插入到哈希桶中
3. 从以下代码中可以看到,HashMap在插入时,并没有处理线程安全问题,因此HashMap不是线程安全的
4. 红黑树优化链表过长是java8新引进,是基于性能的考虑,在冲突大时,红黑树算法会比链表综合表现更好
*/
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        // 1. 桶如果是空的,则进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // 2. (n-1)&hash-->计算桶号,如果当前桶中没有节点,直接插入
        // p来记录桶中的第一个节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;

            // 3. 如果key已经是和桶中第一个节点相等,不进行插入
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode) // 4. 如果该桶中挂接的是红黑树,向红黑树中插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 5. key不同,也不是红黑树,说明当前桶中挂的是一个链表
                // a. 在当前链表中找key
                // b. 如果找到,则不插入
                // c. 如果没有找到,先构建新节点,然后将该节点尾插到链表中
                // d. 检测bitCount的计数,binCount记录的是在未插入新节点前原链表的节点个数
                // e. 新节点插入后,链表长度是否超过TREEIFY_THRESHOLD,如果超过将链表转换为红黑树
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // p已经是最后一个节点,说明在链表中未找到key对应的节点
                       // 进行尾插
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash); // 将链表转化为红黑树
                        break;
                    }

                    // 如果key已经存在,跳出循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            // 如果key已经存在,将key所对节点中的value替换为参数指定value,返回旧value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    /*
注意:afterNodeAccess和afterNodeInsertion主要是LinkedHashMap实现的,HashMap中给出了该方法,但是
并没有实现
*/
    // Callbacks to allow LinkedHashMap post-actions
    // 访问、插入、删除节点之后进行一些处理,
    // LinkedHashMap正是通过重写这三个方法来保证链表的插入、删除的有序性
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
/*
LinkedHashMap: 继承了HashMap,在LinkedHashMap中会对以上方法进行重写,以保证存入到LinkedHashMap中
的key是有序的,注意这里的有序是不自然序列,指的是插入元素的先后次序
LinkedHashMap底层的哈希桶使用的是双向链表
*/

5. Eliminar

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

        // 1. 检测哈希表是否存在
        // 2. index = (n - 1) & hash: 获取桶号
        // 3. p记录当前桶中第一个节点,如果桶中没有节点,直接返回null
        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;

            // 如果第一个节点就是key,用node记录
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                // 如果当前桶下是红黑树,在红黑树中查找,结果用node记录
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    // 当前桶下是链表,遍历链表,在链表中检测是否存在为key的节点
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // node不为空,在HashMap中找到了
            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;
                --size;

                // LinkedHashMap使用
                afterNodeRemoval(node);

                // 删除成功,返回原节点
                return node;
            }
        }

        // 删除失败返回空
        return null;
    }

8. Preguntas frecuentes sobre HashMap

1. Si es nuevo HashMap (19), ¿qué tan grande es la matriz de cubos?

En Java 1.8, lo nuevo no abre espacio para la matriz, sino que solo abre espacio cuando se inserta por primera vez. El espacio abierto es mayor que 19 y más cercano a la potencia de 19, 2^4=16 , 2^5=32, por lo que el tamaño de la matriz de cubetas es 32

2. ¿Cuándo abre HashMap la matriz de cubos para ocupar memoria?

Esta pregunta ya se respondió en la respuesta anterior, y la memoria espacial solo se abre cuando se inserta por primera vez.

3. ¿Cuándo se expandirá hashMap?

Cuando el número de elementos válidos en la tabla >= factor de carga * capacidad de la tabla, la capacidad debe expandirse y la expansión de la capacidad también se realiza de acuerdo con la potencia de 2.

4. ¿Qué sucede cuando dos objetos tienen el mismo código hash?

En get (): si los códigos hash son iguales, primero compare las claves a través del método equal, si las claves son iguales, devuelva el valor directamente, de lo contrario, devuelva nulo

Al insertar: Si el código hash es el mismo, entonces juzgue si la clave existe. Si la clave ya existe, reemplace el valor correspondiente a la clave. Si la clave no existe, insértela.

Al eliminar: si el código hash es el mismo, la clave puede ser la que queremos eliminar, comparar entre iguales, eliminar si es así, devolver si no

5. Si dos claves tienen el mismo código hash, ¿cómo se obtiene el objeto de valor?

Recorra la lista vinculada conectada cuando el valor de hashCode sea igual, hasta que sea igual o nulo

6. ¿Entiendes cuál es el problema con el cambio de tamaño de HashMap? 

Si se cambia la capacidad del HashMap, los nodos en la tabla original deben ser re-hash.El propósito de la expansión es re-hash los nodos y acortar la lista enlazada.

7. ¿Por qué anular los métodos hashcode() y equals()?

Reescribir el código hash: el principio subyacente es calcular el código hash a través de la clave, calcular el hash a través del código hash, el hash devuelve un número entero y luego dividir el resto por este número, y el resultado del cálculo es la posición del cubo . Pero para un tipo personalizado, la clave no se puede convertir en un número entero, y el método hashcode debe reescribirse para convertir la clave del tipo personalizado en un número entero para obtener la posición del depósito.

Anular iguales: cuando se produce un conflicto de hash, es necesario comparar si las claves son las mismas, y el método de igualdad debe usarse para la comparación. Al comparar claves para tipos personalizados, el método de igualdad debe reescribirse para comparar si la el contenido de las llaves es el mismo. 

Supongo que te gusta

Origin blog.csdn.net/qq_58710208/article/details/122154321
Recomendado
Clasificación