¡Explicación detallada de HashMap en Java! Análisis detallado de cómo funciona HashMap y cómo se usa

¡Acostúmbrate a escribir juntos! Este es el primer día de mi participación en el "Nuggets Daily New Plan·Desafío de actualización de abril", haz clic para ver los detalles del evento

Cómo funciona HashMap

  • HashMap almacena pares clave-valor en la implementación de clase interna estática Map.Entry
  • HashMap usa el algoritmo hash, en los métodos put() y get() , se usan los métodos hashCode() y equals()
    • Cuando se llama al método put() pasando un par clave-valor , HashMap usa la clave hashCode() y el algoritmo hash para encontrar el índice donde se almacena el par clave-valor . La entrada se almacena en LinkedList , y si hay una Entrada, se utiliza el método equals() para verificar si la clave pasada existe. Si existe, el valor se sobrescribirá . Si no existe, se creará una nueva Entrada para guardar
    • Cuando llame al método get() pasando la clave , use el método key hashCode() nuevamente para encontrar el índice en la matriz, luego use el método equals() para encontrar la Entrada correcta y devolver el valor de la Entrada

Principio de implementación de HashMap

  • El mapeo de la estructura de datos en Java define la interfaz java.util.Map, que tiene las siguientes cuatro clases de implementación de uso común:
    • Mapa hash:
      • HashMap almacena datos de acuerdo con el valor de hashCode de la clave. Por lo general, puede ubicar directamente el valor correspondiente a la clave de acuerdo con el valor de hashCode de la clave, por lo que tiene una velocidad de acceso rápida.
      • El orden de recorrido en HashMap no está definido
      • En HashMap , la clave de solo un dato puede ser nula como máximo, y el valor de múltiples datos puede ser nulo
      • HashMap no es seguro para subprocesos. Se permite que varios subprocesos escriban en HashMap al mismo tiempo , lo que generará inconsistencias en los datos.
      • Si HashMap quiere cumplir con la seguridad de subprocesos, puede usar el métodosynchronedMap() de Collections para hacer que HashMap sea seguro para subprocesos, o usar ConcurrentHashMap
    • Tabla de picadillo:
      • Hashtable es similar a HashMap , excepto que Hashtable hereda de la clase Dictionary
      • Ni la clave ni el valor en Hashtable pueden ser nulos
      • Hashtable es seguro para subprocesos. Solo se permite que un subproceso escriba en Hashtable al mismo tiempo. Sin embargo , la concurrencia de Hashtable no es tan buena como la de ConcurrentHashMap , que introduce bloqueo de segmento.
        • Hashtable es seguro para subprocesos al agregar bloqueos sincronizados a los métodos
        • ConcurrentHashMap se compone de una estructura de datos de segmento y una estructura de datos de HashEntry
          • Segment是一种可重入锁ReentrantLock,ConcurrentHashMap中作为锁
          • HashEntry用于存储键值对数据
            • 一个ConcurrentHashMap包含一个Segment数组 .Segment的数据结构和HashMap类似,是一种数组和链表的结构
            • 一个Segment中包含一个HashEntry数组.每个HashEntry是一个链表结构的元素
            • 一个Segment守护一个HashEntry数组中的元素
            • HashEntry中的数据进行修改时,必须首先获得HashEntry对应的Segment
        • 分段锁:
          • 分段锁的含义就是用到哪一部分就锁定哪一部分
          • 分段锁就是将整个Map划分成NSegment, 在进行put()get() 操作时,根据键的hashCode值寻找到应该使用哪个Segment. 这个Segment做到了类似HashTable的线程安全
        • ConcurrentHashMap中的键和值都不能为null
      • 不建议使用HashTable, 在不需要线程安全的场景中,可以使用HashMap. 在需要线程安全的场景中,可以使用ConcurrentHashMap
    • LinkedHashMap:
      • LinkedHashMapHashMap的一个子类
      • LinkedHashMap中保存了数据的插入顺序.使用Iterator遍历LinkedHashMap时,首先得到的数据一定是首先插入的
      • LinkedHashMap中可以使用带参的构造函数来按照访问次序进行排序
    • TreeMap:
      • TreeMap实现了SortedMap接口
      • TreeMap可以将保存的数据按照键来进行排序.默认是按照键值进行升序排序,也可以指定排序的比较器.使用Iterator遍历TreeMap时,得到的数据时排序后的数据
      • 如果需要使用排序的映射,建议使用TreeMap. 使用TreeMap时,键必须实现Comparable接口或者是在构造TreeMap时传入自定义的Comparator, 否则会在运行时抛出java.lang.ClassCastException异常

inserte la descripción de la imagen aquí

  • Map类都是要求映射中的键key是不可变对象:
    • 不可变对象就是这个对象创建后,对象的hashCode值不会改变
    • 如果对象的hashCode值发生改变,就很可能无法定位到映射的位置

HashMap的数据结构

  • HashMap使用数组+链表+红黑树的数据结构存储数据的
  • HashMap的内部数据结构是一个桶数组
    • 每一个桶中存放着一个单链表的头节点
    • 每一个节点中存储着一个键值对Entry
  • HashMap采用拉链法解决存在的Hash冲突问题
    • 拉链法: 也就是链地址法,是数组和链表的结合.在每个数组元素上都有一个链表结构,当数据进行Hash之后,得到数组的下标,然后将数据存放到对应下标元素的链表上

inserte la descripción de la imagen aquí

  • Node:
    • NodeHashMap的一个内部类,实现了Map.Entry接口,本质上就是一个键值对映射

HashMap构造函数

  • HashMap中有三个构造函数 : 通常情况下,使用默认的无参构造函数.在能够预估到数据的容量时推荐使用指定容量大小的构造函数
public HashMap();

public HashMap(int initialCapacity);

public HashMap(int initialCapacity, float loadFactor);
复制代码
  • 构造函数中只是设置了几个参数的值,没有对数组和链表进行初始化,在第一次put操作时才调用resize() 方法初始化数组tab. 这样可以很好的节省空间
  • HashMap函数构造过程:
    • 首先,在数组Node[] table
      • length: 初始化长度,默认为16
      • loadFactor: 负载因子,默认为0.75
        • 默认负载因子0.75是对空间和时间效率的平衡性的选择,不建议修改,只有在时间和空间比较特殊的情况下才需要修改:
          • 内存较多但是对时间效率要求很高: 降低负载因子loadFactor的值
          • 内存紧张但是对时间效率要求不高: 增加负载因子loadFactor的值,这个值可以大于1
      • threshold: HashMap中能够容纳的最大数据量的键值对Node个数.
      • t h r mi s h Él yo d = yo mi norte gramo t h yo Él un d F un C t Él r threshold = length * loadFactor : 定义好数组长度之后,负载因子越大,能够容纳的键值对个数越多
        • threshold是对应的数组长度length和负载因子loadFactor允许的最大元素数量,超过这个数量HashMap就会重新扩容resize. 扩容后的HashMap容量是当前容量的两倍

HashMap重要方法

hash(K)

	static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
复制代码
  • JavaHashMap中,没有直接使用hashcode() 作为HashMap中的hash
  • HashMap中将hashcode() 的值无符号右移16位得到一个新值,然后将hashcode() 的值和这个新值进行异或运算得到最终的hash值保存在HashMap中. 这样可以避免哈希碰撞
    • 比如容量大小n16,n-115(0x1111), 散列值真正生效的只是低4位,此时新增的键的hashcode() 的值如果是2,18,34这样以16的倍数为差的等差数列时,就会产生大量的哈希碰撞
    • 使用这样的方法,将高16位和低16位进行异或,因为大部分hashcode() 的值分布已经很均匀了,即使发生碰撞也用 O ( l o g n ) O(logn) 时间复杂度的红黑树进行了优化.这样通过使用异或的方法,不仅减少了系统开销,也不会因为tab长度较小时高位没有参与下标的运算引发哈希碰撞

put(K, V)

  • 使用put(K, V) 操作时 ,HashMap计算键值K的哈希值,然后将这个键值对Entry放入到HashMap中对应的桶bucket
  • 然后寻找以当前桶为头结点的一个单链表,顺序遍历单链表找到某个节点的Entry中的key等于给定的参数K
  • 如果找到则将旧的参数值V替换为参数指定的V. 否则直接在链表的尾部插入一个新的Entry节点
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 
复制代码
  • HashMap中重写equals() 方法必须也要重写hashcode() 方法:
    • 根据hash值,定位到数组某个位置后,向位置中后面的链表添加元素时,判断元素是否一样中,首先判断hash值是否相等,然后再判断equals()
    • 如果只对equals() 进行重写,不对hashcode() 进行重写时,依然会按照不同的两个对象处理,所以重写equals() 方法时必须也要重写hashcode() 方法
  • HashMap中既要判断hash值,也要使用equals() 方法判断:
    • HashMap中链表结构进行遍历判断时,重写的equals() 方法判断对象是否相等的业务逻辑比较复杂,这样下来的循环遍历判断影响性能
    • HashMap中将hash值的判断放在前面,只要hash值不同,整个条件就是false, 不需要进行equals() 方法判断对象是否相等,提升了HashMap的性能
    • HashMap中是根据hashcode() 的值定位到数组的位置的,同一个数组位置中后面的链表中元素的hashcode() 的值都相同.比较hashcode() 的值没有意义,因为必定相等 .HashMap中没有直接使用hashcode() 的值,用的是对hashcode() 的值进行移位和异或运算后的hash值,这里比较的是元素的hash

resize()

  • 初始化HashMap时,按照阈值threshold分配内存
  • 如果HashMap中的数据记录超过HashMap的阈值就会进行扩容
  • 扩容时,数组会采用将数组容量大小的值左移一位的算法将将数组扩容至两倍
  • 扩容时,根据数据的hash值与数组长度进行逻辑与运算,根据运算结果是否为0来决定数据是不动还是将数组索引位置变更为当前索引位置和原数组长度之和
  • 扩容时不会重新计算hash,keyhash值会保存在数组位置的后面的node节点元素中

treeifyBin()

  • 数组中单个链表长度超过8, 数组的长度超过64时才会进行链表结构到红黑树结构的转换,否则只是进行扩容操作
  • HashMap中,使用红黑树结构占用空间大,尽可能不使用红黑树结构

get(K)

  • HashMap通过计算键的哈希值,寻找到对应的桶bucket, 然后顺序遍历桶bucket存放的单链表,通过比对Entry的键找到对应的哈希值
  • 如果对应位置后面是红黑树结构就在红黑树结构中查找,如果是链表结构就遍历链表,查询需要找的对象
    • 红黑树遍历的时间复杂度 : O ( l o g n ) O(logn)
    • 链表遍历的时间复杂度 : O ( n ) O(n)

Hash冲突

  • Hash冲突:
    • 因为Hash是一种压缩映射,这样每一个Entry节点无法对应到一个只属于自身的桶bucket
    • 必然会存在多个Entry共用一个桶bucket, 拉成一个链条的情况.这种情况就是Hash冲突
  • Hash冲突存在的问题:
    • Hash冲突的极端情况下,某一个桶bucket后面挂着的链表会特别长,导致遍历的效率很低
  • Hash冲突无法完全避免,为了提高HashMap的性能,需要尽量缓解Hash冲突来缩短每个桶的外挂链表的长度
    • HashMap中存储的Entry较多时,需要对HashMap扩容来增加桶bucket的数量
    • 这样对后续要存储的Entry来讲,就会大大缓解Hash冲突

HashMap总结

HashMap中MAXIMUM_CAPACITY设置为1<<30

  • MAXIUM_CAPACITY:
    • int类型,表示HashMap的最大容量
    • 使用 << 移位运算的结果不能超过int类型表示的最大值
    • 使用1左移 << 运算时最大只能左移30位,否则就会溢出
  • Java中的int类型占4个字节,每个字节占用8位,所以int类型占用32
  • Java中的int类型是有符号的,使用第1位作为符号位,此时还有31位,这时使用1左移只能左移30

HashMap中容量设置为2的整数幂次方

  • 通过限制一个数组长度length2的整数幂次方的数,这样使得 (length - 1) & hh % length 的结果是一致的
  • HashMap中将容量设置为2的整数幂次方主要就是为了在取模和扩容时做优化,同时减少冲突
	final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            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;
    }
复制代码
  • tab[(n - 1) & hash] : 根据hash值快速定位到数组的位置
    • 数组tab
    • 数组长度n
    • 需要查找的key对应的值hash
      • 因为数组长度n设置为2的整数幂次方,这样初始情况下n-1转换为2进制时各个位上都是1
      • 此时使用 & 与对应的值hash进行运算时的结果就和hash值一样,也就快速定位到了数组中的位置
      • 使得数组中的数据更加分散,减少碰撞
  • 如果数组长度不是设置为2的整数幂次方:
    • 数组长度在初始情况下使用n-1转换为2进制时,存在0位,导致很多位置无法放置元素,造成空间浪费
    • 数组的有效使用位置大量减少,增加了碰撞几率,减慢了查询速度

HashMap中的负载因子设置为0.75

  • 泊松分布: Poisson分布.描述某段时间内,事件具体的发生概率

P ( X = k ) = λ k k ! e λ , k = 0 , 1 , ( λ 是均值 , k 为发生次数 ) P(X=k)=\frac{\lambda^k}{k!}e^{-\lambda},k=0,1,…(\lambda是均值,k为发生次数) inserte la descripción de la imagen aquí

  • TreeNode占用的空间是常规节点的两倍,所以只有当箱子bin(数组中的一个桶)中元素的数量超过TREEIFY_THRESHOLD时才会需要使用TreeNode
  • HashMap中的hash值分布比较均匀时,很少使用到TreeNode
  • 在随机hashcode情况下 ,bin中节点出现的频率遵循泊松Poisson分布,此时负载因子为 0.75, 均值 λ {\lambda} 0.5
  • 如果调整负载因子的值,均值 λ {\lambda} 会出现较大偏差
  • HashMap扩容到32或者64时,一个箱子bin中存储8个数据量的概率为0.00000006. 所以当一个箱子中节点数目大于等于8个时,可以将HashMap中桶中的数据从链表结构转换为树结构存储,效果是最好的

HashMap中的元素尽量使用迭代器Iterator遍历

  • 在迭代器Iterator中使用的fail-fast策略,在遍历发生线程并发时,可以立即抛出异常
  • fail-fast策略:
    • HashMap是非线程安全的
    • 使用迭代器Iterator过程中,如果其余的线程同时也在修改HashMap, 就会立即抛出ConcurrentModificationException异常
  • fail-fast策略实现:
    • fail-fast策略通过modCount实现
    • modCount记录修改次数
    • 在迭代器初始化过程中将modCount的值赋值迭代器的expectedModCount
    • 在迭代器迭代过程中,判断modCountexpectModCount是否相等.如果不相等就说明有其余线程对HashMap进行了修改
Map<Integer, Integer> hashMap = new HashMap<Integer, Integer>();
Iterator<Map.Entry<Integer, Integer>> entries = hashMap.entrySet().iterator();
while (entries.hasNext()) {
	Map.Entry<Integer, Integer> entry = entries.next();
	System.out.println("KEY = " + entry.getKey() + ", VALUE = " + entry.getValue());
}
复制代码

HashMap的使用特点

  • HashMap中的扩容是一个特别损耗性能的操作,所以在初始化HashMap时,应该估算HashMap的大小,确定一个大致的数值,避免在使用HashMap时频繁初始化
  • HashMap是线程不安全的,在并发的环境中建议使用ConcurrentHashMap
  • La introducción del árbol rojo-negro en Java 8 optimiza en gran medida el rendimiento de HashMap . Se refleja principalmente en que cuando el algoritmo hash es desigual, es decir, cuando la lista enlazada en el método zip es muy larga, la lista enlazada puede ser convertido en una estructura de árbol rojo-negro.En este momento, la complejidad del algoritmo está dada por O ( n ) Sobre) cae a O ( l o g n ) O (iniciar sesión)

Supongo que te gusta

Origin juejin.im/post/7084947689011413028
Recomendado
Clasificación