Java中HashMap详解

Posted 攻城狮Chova

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中HashMap详解相关的知识,希望对你有一定的参考价值。

HashMap的实现原理

HashMap的数据结构

  • HashMap使用数组+链表+红黑树的数据结构存储数据的
  • HashMap的内部数据结构是一个桶数组
    • 每一个桶中存放着一个单链表的头节点
    • 每一个节点中存储着一个键值对Entry
  • HashMap采用拉链法解决存在的Hash冲突问题

HashMap构造函数

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

public HashMap(int initialCapacity);

public HashMap(int initialCapacity, float loadFactor);
  • 构造函数中只是设置了几个参数的值,没有对数组和链表进行初始化,在第一次put操作时才调用resize() 方法初始化数组tab. 这样可以很好的节省空间

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) 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(logn)
    • 链表遍历的时间复杂度 : O ( n ) 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 的结果是一致的
	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为发生次数) P(X=k)=k!λkeλ,k=0,1,(λ,k)
  • 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中桶中的数据从链表结构转换为树结构存储,效果是最好的

以上是关于Java中HashMap详解的主要内容,如果未能解决你的问题,请参考以下文章

Java集合详解4:HashMap和HashTable

Java中HashMap详解

Java中HashMap详解

(转) Java中的负数及基本类型的转型详解

Java7 HashMap详解

Java集合详解:HashMap原理解析