HashMap 的底层实现(判断元素存放的位置,解决哈希冲突,优化扩容)

HashMap概述

HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。多线程环境中推荐ConcurrentHashMap。

HashMap的底层实现

散列表

在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起(即散列表),发挥两者各自的优势

散列表:数组+链表,数组中保存的数据是链表,整合了数组的快速索引index和链表的动态扩容;有散列表就必须要有哈希,
hash基本原理是把任意长度的输入,通过hash算法变成固定长度的输出。从原始数据映射后的二进制串就是哈希值。
在这里插入图片描述

hash的特点:
● 从hash值不可以反向推导出原始的数据
● 输入的数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
● hash算法的执行效率要高效,长的文本也能快速的计算出哈希值
● hash算法的冲突概率要小(hash冲突可以联想抽屉原理)

hashmap:
在这里插入图片描述

JDK1.8之前(判断元素存放的位置,解决哈希冲突)

JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 散列表。一个长度为16的数组中,每个元素存储的是一个链表的头结点。
判断当前元素存放的位置
HashMap 基于 Hash 算法实现的,通过 put(key,value) 存储元素,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标。 当传入 key 时,HashMap 会根据 key,调用 hash(Object key) 方法,计算出 hash 值,根据 hash 值将 value 保存在 Node 对象里,Node 对象保存在数组里。(当计算出的 hash 值相同时,称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value)
元素在数组中的下标一般情况是通过 hash(key.hashCode())%length 获得,也就是元素的key的哈希值数组长度取模得到,或者hash&(length-1)。
计算机中直接求余效率不如 位移运算,源码中做了优化 hash&(length-1)。 要想保证hash%length==hash&(length-1),那么length必须是2的n次方

两个问题:

HashMap的长度为什么是2的幂次方?

为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648到2147483647,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。
如果是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。

实际中通过 hash&(length - 1) 而不用hash%length判断当前元素存放的位置。 是因为采⽤⼆进制位操作 &(位移运算),相对于%能够提⾼运算效率,如果当取余(%)操作中如果除数是2的幂次,则等价于与其除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length是2的n 次⽅)这就解释了 HashMap 的⻓度为什么是2的幂次⽅。

当数组 length很小时,让key的hash值高16位也参与路由运算 hash&(length-1)=index(路由寻址的下标)的原因?

当数组的长度很短时,只有低位数的hashcode值能参与运算。而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率。并且使得高16位和低16位的信息都被保留了。
然后有不少博客提到了因为int是4个字节,所以右移16位。原因大家可以打开hashmap的源码,找到hash方法,按住ctrl点击方法里的hashcode,跳转到Object类,然后可以看到hashcode的数据类型是int。int为4个字节,1个字节8个比特位,就是32个比特位,所以16很可能是因为32对半的结果,也就是让高的那一半也来参与运算

解决哈希冲突
JDK1.8之前采用的是拉链法。
如果计算出 要存入数组中某个位置的元素 的 hash 值与这个位置的元素hash 值相同,称为 hash 冲突。HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。即通过拉链法解决冲突
所谓 “拉链法” 就是:创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
当我们往Hashmap中put元素时,通过 put(key,value) 存储元素。
● 存储时,如果出现hash值相同的key,此时有两种情况:

  1. 如果key相同,则覆盖原始值;
  2. 如果key不同(称为 hash 冲突),则将当前的key-value放入链表中
    ● 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
    所以HashMap解决hash冲突问题的核心就是:使用了数组的存储方式,将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比
    (JDK1.8之后HashMap 解决哈希冲突的做法是用链表和红黑树存储相同 hash 值的 value)

JDK1.8之后(解决哈希冲突,扩容原理)

JDK1.8 之后HashMap采用 数组+链表+红黑树 实现 ,在解决哈希冲突时有了较⼤的变化。
解决hash冲突
hashmap 发生冲突时会形成链表,当某个链表的长度大于阈值(默认为8)时,那么这个链表就会树化成红黑树,以减少搜索时间(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)所以hashmap底层结构其实是数组+链表+红黑树
出现红黑树就是为了解决出现碰撞时链化链得很长的问题(当元素越来越多的时候,hashMap的查找速度就从O(1)升到O(n),导致链化严重)红黑树是一个自平衡的二叉查找树,提高查找效率从原来的O(n)到O(logn)。TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都⽤到了红⿊树。红⿊树就是为了解决⼆叉查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构,严重影响搜索效率。
扩容优化
当链表的长度超过阈值8,而数组长度并未超过64个时就选择对数组进行扩容(数组默认长度为16,之后每次扩充,容量变为原来的 2 倍
hashmap扩容原理:为了解决哈希冲突导致的链化严重从而导致查找效率低的问题,我们提供了扩容机制。把数组长度扩大,桶位多了,每个桶位存放的链表长度就减少了,那么查询的效率就会提升;

Hashmap JDK1.7 VS JDK1.8 比较

JDK1.8主要解决或优化了一下问题:
● resize 扩容优化
● 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
● 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

不同 JDK 1.7 JDK 1.8
存储结构 数组 + 链表 数组 + 链表 + 红黑树
初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()中
hash值计算方式 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则 无冲突时,存放数组;冲突时,存放链表 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李莲花*

多谢多谢,来自一名大学生的感谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值