HashMap底层实现原理

简介: HashMap底层实现原理

来吧,说到HashMap,想必各位java开发者应该或多或少的都使用过吧,只停留在用,就没想过它的底层是怎么实现的吗?当你真正去开始了解他的底层实现的时候,你会发现,真的是设计的好巧妙呀,小编也是突发奇想,分享下自己的理解吧,如果有不对的,欢迎大家联系小编指正修改,谢谢大家。

说到HashMap,他的jdk1.7和1.8的底层实现是发生了变化的,首先我们看看1.7的吧,首先我们要明白两个概念,数组(采用一段连续的存储单元来存储数据)和链表(可以不连续,存储单元可能包含前后节点数据的地址,有单向和多向的区别)是什么,不过多展开了,自己去了解下吧。


1.7的HashMap底层结构是,数组加链表的形式,咱们一起来看看为啥是这种形式的吧,我们存一个key value的数据到map的时候,第一步会把key通过hash函数(我擦,写文章好难啊,每一句话都要解释下,hash函数是什么以及算法在后面有段落介绍,先让我把主要原理一次说完),算出一个数组的下标值,如果这个数组的这个下标里没数据,就把刚才的key value的数据存到这个数组下标这里如图:

细心的小伙伴已经发现问题了呗,如果hash函数算出的结果一样怎么办,你这数组存不下呀,对的,这个现象叫hash冲突,所以链表就来了,每一个数组下面存的都是一个链表,如果发生冲突,就依次往链表里一个一个的存,这就是1.7的HashMap的底层实现原理。

存的时候,因为HashMap不允许key重复,会涉及到key的比较,如果key是我们常用的,String,Integer这些,他们都是已经重写过equals方法了,如果key是自定义的类,就必须重写equals和hashCode函数,为啥要重写俩,是因为:1、如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;2、如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)。

每一个entry就是一个key value的数据,实际通过key取数据的时候,步骤是这样的,先去算key的hash函数算出所在的位置,然后拿着key去这个链表里一个一个的用equals方法比,相同的时候把value取出来结束。

我们再来看看1.8的时候,HashMap底层结构发生了什么变化,最大的区别在于引入了红黑树(又来一个需要解释的,自己去查吧这个纯数据结构的名词),整体的结构变成了下图这个样子:

先说结论吧,当链表长度大于8并且数组长度大于64时,会把链表转成红黑树,为啥要引入红黑树,主要是为了查找的时候查的快,想过一个问题没,如果链表的长度足够的长(其实是小概率事件,这里要提到一个概率论中的泊松分布,因为链表长度大于等于8时转成红黑树正是遵循泊松分布,概率已经很小了),红黑树是平均查到的次数是log(n),长度为8的时候,平均查找长度为3链表长的话,每次查找一个元素都要花很多次遍历很多次才能查到,链表长度为8的时候平均查找长度是几呢?咱们不妨推理一下


这个图片说的明白不,理解不了最后公式的话自己代入一个数算算,比如n=100,1加到100是5050,除以100等于50.5,是不是等于(100+1)除以2,所以,如果链表长度为8时(n+1)/2=4.5次,当n足够大时,平均查找次数约等于n/2=8/2=4,链表查询的次数4已经高于红黑树3了。

明白了吧,又有朋友看出问题了,为什么不是6(6/2=3和log(6)约等于3)的时候就转红黑树,因为吧,当链表转红黑树的时候是需要花费很多时间的,如果次数差不多的情况下,用链表可以节省转换红黑树的时间,那为什么不是7呢?还有一种情况,链表的数据发生了删除和新增,如果是7,频繁的发生链表和红黑树的转换也是不可取的,所以7可以当成一个缓冲区,避免在查找性能差不多的情况下,发生太多的链表红黑树的转换浪费时间,所以8的时候是最合适的。

Hash函数算法:这个就是对key进行的计算的函数,说到这个就必须提到HashMap的初始容量16,会自动扩容,当元素个数超过容量的0.75时就会进行扩容,每次扩容两倍,也就是说每次它的容量都会是2的次幂,为什么这么搞呢,其实是因为Hash函数采取了位运算的方式如下:

公式:index = HashCode(Key) & (Length - 1)其中Length是HashMap的长度,假设我们的key叫fym

  1. 计算fym的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001
  2. 假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111
  3. 把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。


可以说,Hash函数算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。好了,问题来了,为什么用16作为初始容量,为什么不是10,咱们用10来看看结果是啥样子的Length - 1=9 二进制是1001与上面的结果后四位1001进行与运算 结果还是1001,在换个hashCode1011 和1111结果同样是1001,意味着什么,看明白了吗,意味着很多key的hash函数最后的计算结果都是同一个数组下标,这不就是hash冲突吗。而且有些下标永远不会出现,比如0111无论谁和9的二进制1001与都不会出现0111,这就导致了数组有些空间的浪费哦,再回看为什么每次它的容量都会是2的次幂,因为2次幂的Length - 1都是一堆111111111,可以根据hashCode保证均匀分布,减少hash冲突和数组个别空闲的问题。


HashMap的扩容步骤:创建一个新的Entry空数组,长度是原数组的2倍

遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。


说到了扩容,就不得不提,HashMap的线程安全问题,HashMap是非线程安全的,假设一个HashMap已经到了Resize(包含扩容和ReHash两个步骤)的临界点。此时有两个线程A和B,在同一时刻对HashMap进行Put操作,ReHash在并发的情况下可能会形成链表环,此时问题还没有直接产生,当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于链表环所在的位置的时候,由于位置有环形链表,所以程序将会进入死循环。所以他是非线程安全的,线程安全HashTable它的方法是同步的,效率很低,现在我们大多用ConcurrentHashMap(下回具体介绍),最后在补充几一点吧,在HashMap中,null可以作为键,这样的键只能有一个;在HashTable中,无论键key还是值value都不能为null 。
最后,大家想继续了解相关知识的,可以继续关注公众号,不定时推送。分享了这么牛逼的知识,还不请小编喝个水吗,哈哈哈,欢迎土豪直接赏赞,谢谢,您的支持就是小编最大的动力。

相关文章
|
6月前
|
存储 缓存 安全
面试题-HashMap底层原理与HashTable的区别
字节跳动面试题-HashMap底层原理与HashTable的区别
63 0
|
3月前
|
存储 算法 Java
【Java集合类面试八】、 介绍一下HashMap底层的实现原理
HashMap基于hash算法,通过put和get方法存储和获取对象,自动调整容量,并在碰撞时用链表或红黑树组织元素以优化性能。
|
24天前
|
存储 算法 安全
HashMap的实现原理,看这篇就够了
关注【mikechen的互联网架构】,10年+BAT架构经验分享。深入解析HashMap,涵盖数据结构、核心成员、哈希函数、冲突处理及性能优化等9大要点。欢迎交流探讨。
HashMap的实现原理,看这篇就够了
|
1月前
|
机器学习/深度学习 算法
让星星⭐月亮告诉你,HashMap之tableSizeFor(int cap)方法原理详解(分2的n次幂和非2的n次幂两种情况讨论)
`HashMap` 的 `tableSizeFor(int cap)` 方法用于计算一个大于或等于给定容量 `cap` 的最小的 2 的幂次方值。该方法通过一系列的无符号右移和按位或运算,逐步将二进制数的高位全部置为 1,最后加 1 得到所需的 2 的幂次方值。具体步骤包括: 1. 将 `cap` 减 1,确保已经是 2 的幂次方的值直接返回。 2. 通过多次无符号右移和按位或运算,将最高位 1 后面的所有位都置为 1。 3. 最终加 1,确保返回值为 2 的幂次方。 该方法保证了 `HashMap` 的数组容量始终是 2 的幂次方,从而优化了哈希表的性能。
33 1
|
2月前
|
设计模式 安全 Java
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
假如有T1、T2两个线程同时对某链表扩容,他们都标记头结点和第二个结点,此时T2阻塞,T1执行完扩容后链表结点顺序反过来,此时T2恢复运行再进行翻转就会产生环形链表,即B.next=A;采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。JDK8中,HashMap采用尾插法,扩容时链表节点位置不会翻转,解决了扩容死循环问题,但是性能差了一点,因为要遍历链表再查到尾部。例如15(即2^4-1)的二进制为1111,31的二进制为11111,63的二进制为111111,127的二进制为1111111。
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
|
1月前
|
存储 算法 索引
HashMap底层数据结构及其增put删remove查get方法的代码实现原理
HashMap 是基于数组 + 链表 + 红黑树实现的高效键值对存储结构。默认初始容量为16,负载因子为0.75。当存储元素超过容量 * 负载因子时,会进行扩容。HashMap 使用哈希算法计算键的索引位置,通过链表或红黑树解决哈希冲突,确保高效存取。插入、获取和删除操作的时间复杂度接近 O(1)。
29 0
|
1月前
|
存储 算法 安全
HashMap常见面试题(超全面):实现原理、扩容机制、链表何时升级为红黑树、死循环
HashMap常见面试题:红黑树、散列表,HashMap实现原理、扩容机制,HashMap的jd1.7与jdk1.8有什么区别,寻址算法、链表何时升级为红黑树、死循环
|
5月前
|
存储 Java 测试技术
滚雪球学Java(66):Java之HashMap详解:深入剖析其底层实现与源码分析
【6月更文挑战第20天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
45 3
滚雪球学Java(66):Java之HashMap详解:深入剖析其底层实现与源码分析
|
6月前
|
存储 Java 索引
Java HashMap:设计思想与实现原理详解
Java HashMap:设计思想与实现原理详解
255 0
|
5月前
|
存储 算法 Java
HashMap 的实现原理
HashMap 的实现原理