【Java集合】 HashMap底层原理 和 Hash冲突的解决方法

HashMap

HashMap底层数据结构

底层数据结构:hash表数据结构,即数组+链表|红黑树

  1. 往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,当出现hash相同的key
  • 如果key相同,则覆盖原始值
  • 如果key不相同(hash冲突),则将当前数据放入链表或红黑树中
  1. 获取数据时,对key进行hash运算,找到数组中对象的hash值下标,在进一步判断key是否相同,从而找到对应的值

​ 在 JDK1.8之前, Hashmap使用链表来解决哈希冲突。当哈希冲突较多时,链表中的元素增多,査找、插入和删除的时间复杂度从 O(1)退化为 O(n)。因此在 JDK1.8引入红黑树,将链表长度超过一定國值(链表长度>=8 && 数组长度>= 64)时的链表转换为红黑树,避免性能急剧下降。当链表长度降到6以下时,红黑树会重新退化为链表,保持简单高效。

··

HashMap的扩容操作是怎么实现的?

  • 不管是JDK1.7或者JDK1.8 当put方法执行的时候,如果table为空,则执行resize()方法扩容。默认长度为16
  • 扩容因子为0.75,以2的n次方扩容,最高可扩容30次。如第一次是长度达到16**0.75*=12的时候开始扩容,16*2^1=32。
  • HashMap 采用2的n次方倍作为容量,主要是为了提高哈希值的分布均匀性和哈希计算的效率
  • Hashmap通过(n- 1)&hash 来计算元素存储的索引位置,这种位运算只有在数组容量是2的n次方时才能确保索引均匀分布。位运算的效率高于取模运算(hash %n),提高了哈希计算的速度。
  • 当 Hashap 扩容时,通过容量为2的 n次方,扩容时只需通过简单的位运算判断是否需要迁移,这减少了重新计算哈希值的开销,提升了rehash 的效率
JDK1.7版本扩容
  1. 先生成新数组;
  2. 遍历老数组中的每个位置上的链表上的每个元素;
  3. 获取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标;
  4. 将元素添加到新数组中去;
  5. 所有元素转移完之后,将新数组赋值给HashMap对象的table属性。
JDK1.8版本扩容
  1. 先生成新数组;
  2. 遍历老数组中的每个位置上的链表或红黑树;
  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去;
  4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置;
    • 统计每个下标位置的元素个数;
    • 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置;
    • 如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置;
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性。

··

JDK1.8对红黑树进行的改动

  • 改进了哈希函数的计算:(JDK1.7时使用indexFor方法确定元素的位置,JDK1.8时采用(n-1) & hash )JDK1.8 中优化了哈希函数,使得哈希值的分布更加均匀,减少了哈希冲突的发生。通过在生成哈希值时使用“扰动函数”,确保哈希值的高低位都能参与到桶的选择中。
  • 扩容机制优化:JDK1.8改进了扩容时的元素迁移机制。在扩容过程中不再对每个元素重新计算哈希值,而是根据原数组长度的高位来判断元素是留在原位置还是迁移到新数组中的新位置。这一改动减少了不必要的计算,提升了扩容效率。
  • 头插法变为尾插法:头插法的好处就是插入的时候不需要遍历链表,直接替换成头结点,但是缺点是扩容的时候会逆序,而逆序在多线程操作下可能会出现环,产生死循环,于是改为尾插法。

··

解决hash冲突的四种方法

  • 链地址法 (HashMap)
  • 再哈希法
    • fi=(f(key)+i*g(key)) % m (i=1,2,……,m-1), 其中,f(key) 和 g(key) 是两个不同的哈希函数,m为[哈希表]的长度
    • 第二个函数 g(key) 确定移动的步长因子
  • 建立公共溢出区
    • 基础表和溢出表。将所有关键字通过哈希函数计算出相应的地址。然后将未发生冲突的关键字放入相应的基础表中,一旦发生冲突,就将其依次放入溢出表中即可。
  • 开放定址法
    • 线性探查 - 发生冲突后,顺序查找下一个空余的槽位
    • 二次探测 - 左右探测空余的槽位
    • 伪随机探测 - 这种方法即是产生一些随机系列值,并给定随机数作为起点。
HashMapJava 中最常用的集合之一,它基于哈希表实现,可以提供快速的键值对查找插入。下面是 HashMap底层原理: 1. 数据结构:HashMap 内部使用一个数组来保存元素,每一个元素都是一个键值对对象 Entry<K,V>,其中 K V 分别表示键值。每一个 Entry 对象包含一个 hash 值、一个键对象一个值对象以及一个指向下一个 Entry 对象的指针。 2. 哈希函数:HashMap 使用的是 JDK 提供的默认哈希函数,即 key.hashCode(),它将 key 转换为一个整数,这个整数就是该 key 的哈希值。 3. 哈希冲突:由于哈希函数的不完美性,不同的 key 可能会产生相同的哈希值,这就是哈希冲突。当两个不同的 key 产生相同的哈希值时,HashMap 会在同一个桶中保存它们,这个桶中保存的是一个链表,每一个链表节点就是一个 Entry 对象,它包含了一个键值对一个指向下一个 Entry 对象的指针。 4. 扩容:当 HashMap 中元素的数量超过了负载因子(默认为 0.75)乘以数组大小时,就需要对数组进行扩容。扩容操作会重新计算每个元素的哈希值,并将它们放到新的桶中。同时,为了减少哈希冲突,新数组的大小会是原数组大小的两倍,并且新数组中的每个桶都会是一个链表头节点。 5. 并发问题:HashMap 是非线程安全的,如果多个线程同时修改 HashMap,可能会导致链表出现环形结构,从而导致死循环。为了解决这个问题,Java 提供了一个线程安全的 ConcurrentHashMap 类,它使用了分段锁来保证线程安全。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值