《吊打面试官》系列-HashMap

你知道的越多,你不知道的越多java


点赞再看,养成习惯git


本文 GitHub github.com/JavaFamily 上已经收录,有一线大厂面试点思惟导图,也整理了不少个人文档,欢迎Star和完善,你们面试能够参照考点复习,但愿咱们一块儿有点东西。github

前言

做为一个在互联网公司面一次拿一次Offer的面霸,战胜了无数竞争对手,每次都只能看到无数落寞的身影失望的离开,略感愧疚(请容许我使用一下夸张的修辞手法)。面试

因而在一个寂寞难耐的夜晚,我痛定思痛,决定开始写互联网技术栈面试相关的文章,但愿能帮助各位读者之后面试势如破竹,对面试官进行360°的反击,吊打问你的面试官,让一同面试的同僚瞠目结舌,疯狂收割大厂Offer!算法

全部文章的名字只是个人噱头,咱们应该有一颗谦逊的心,因此但愿你们怀着空杯心态好好学,一块儿进步。数组

正文

一个婀娜多姿,穿着衬衣的小姐姐,拿着一个精致的小笔记本,径直走过来坐在个人面前。安全


看着眼前这个美丽的女人,心想这不会就是Java基础系列的面试官吧,真香。微信


不过看样子这么年轻应该问不出什么深度的吧,嘻嘻。(哦?是么😏)数据结构

小伙子,听前面的面试官说了,你Redis和消息队列都回答得不错,看来仍是有点东西。多线程

美丽迷人的面试官您好,您见笑了,全靠看了敖丙的《吊打面试官》系列,否则我还真的回答不上不少本来的知识盲区,他真的有点东西。

面试官心想:哦,吊打面试官是么,那今天我就让你知道,吊打这两个字怎么写的吧,年轻人啊,提早为你感到可惜。


嗯嗯小帅比,虽然前面的技术栈没啥太大的瑕疵,不过将来很长的一段时间我会用一期期的基础教你作人的,你要准备好哟!


好了咱们开始今天的面试吧,小伙子你了解数据结构中的HashMap么?能跟我聊聊他的结构和底层原理么?

切,这也太看不起我了吧,竟然问这种低级问题,不过仍是要好好回答。

嗯嗯面试官,我知道HashMap是咱们很是经常使用的数据结构,由数组和链表组合构成的数据结构。

大概以下,数组里面每一个地方都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node。

由于他自己全部的位置都为null,在put插入的时候会根据key的hash去计算一个index值。

就好比我put(”帅丙“,520),我插入了为”帅丙“的元素,这个时候咱们会经过哈希函数计算出插入的位置,计算出来index是2那结果以下。

hash(“帅丙”)= 2

你提到了还有链表,为啥须要链表,链表又是怎么样子的呢?

咱们都知道数组长度是有限的,在有限的长度里面咱们使用哈希,哈希自己就存在几率性,就是”帅丙“和”丙帅“咱们都去hash有必定的几率会同样,就像上面的状况我再次哈希”丙帅“极端状况也会hash到一个值上,那就造成了链表。

每个节点都会保存自身的hash、key、value、以及下个节点,我看看Node的源码。

说到链表我想问一下,你知道新的Entry节点在插入链表的时候,是怎么插入的么?

java8以前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,就像上面的例子同样,由于写这个代码的做者认为后来的值被查找的可能性更大一点,提高查找的效率。

可是,在java8以后,都是所用尾部插入了。

为啥改成尾部插入呢?

这!!!这个问题,面试官可真会问!!!还好我饱读诗书,否则死定了!

有人认为是做者随性而为,没啥luan用,其实否则,其中暗藏玄机

首先咱们看下HashMap的扩容机制:

帅丙提到过了,数组容量是有限的,数据屡次插入的,到达必定的数量就会进行扩容,也就是resize。

何时resize呢?

有两个因素:

  • Capacity:HashMap当前长度。
  • LoadFactor:负载因子,默认值0.75f。

怎么理解呢,就好比当前的容量大小为100,当你存进第76个的时候,判断发现须要进行resize了,那就进行扩容,可是HashMap的扩容也不是简单的扩大点容量这么简单的。

扩容?它是怎么扩容的呢?

分为两步

  • 扩容:建立一个新的Entry空数组,长度是原数组的2倍。
  • ReHash:遍历原Entry数组,把全部的Entry从新Hash到新数组。

为何要从新Hash呢,直接复制过去不香么?

卧槽这个问题!有点知识盲区呀!

1x1得 1 1x2 得 2 …. 有了,我想起来敖丙那天晚上在我耳边的话了:假如我年少有为不自卑,懂得什么是珍贵,那些好梦没给你,我一辈子有愧….什么鬼!

小姐姐:是由于长度扩大之后,Hash的规则也随之改变。

Hash的公式---> index = HashCode(Key) & (Length - 1)

原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不同了。

扩容前:

扩容后:

说完扩容机制咱们言归正传,为啥以前用头插法,java8以后改为尾插了呢?

卧槽,我觉得她忘记了!竟然仍是被问到了!

我先举个例子吧,咱们如今往一个容量大小为2的put两个值,负载因子是0.75是否是咱们在put第二个的时候就会进行resize?

2*0.75 = 1 因此插入第二个就要resize了

如今咱们要在容量为2的容器里面用不一样线程插入A,B,C,假如咱们在resize以前打个短点,那意味着数据都插入了可是还没resize那扩容前多是这样的。

咱们能够看到链表的指向A->B->C

Tip:A的下一个指针是指向B的

由于resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,经过从新计算索引位置后,有可能被放到了新数组的不一样位置上。

就可能出现下面的状况,你们发现问题没有?

B的下一个指针指向了A

一旦几个线程都调整完成,就可能出现环形链表

若是这个时候去取值,悲剧就出现了——Infinite Loop。

诶卧槽,小伙子难不倒他呀!

小伙子有点东西呀,可是你都都说了头插是JDK1.7的那1.8的尾插是怎么样的呢?

由于java8以后链表有红黑树的部分,你们能够看到代码已经多了不少if else的逻辑判断了,红黑树的引入巧妙的将本来O(n)的时间复杂度下降到了O(logn)。

Tip:红黑树的知识点一样很重要,仍是那句话不打没把握的仗,限于篇幅缘由,我就不在这里过多描述了,之后写到数据结构再说吧,不过要面试的仔,仍是要准备好,反正我是常常问到的。

使用头插会改变链表的上的顺序,可是若是使用尾插,在扩容时会保持链表元素本来的顺序,就不会出现链表成环的问题了。

就是说本来是A->B,在扩容后那个链表仍是A->B

Java7在多线程操做HashMap时可能引发死循环,缘由是扩容转移后先后链表顺序倒置,在转移过程当中修改了原来链表中节点的引用关系。

Java8在一样的前提下并不会引发死循环,缘由是扩容转移后先后链表顺序不变,保持以前节点的引用关系。

那是否是意味着Java8就能够把HashMap用在多线程中呢?

我认为即便不会出现死循环,可是经过源码看到put/get方法都没有加同步锁,多线程状况最容易出现的就是:没法保证上一秒put的值,下一秒get的时候仍是原值,因此线程安全仍是没法保证。

小伙子回答得很好嘛,这都被你回答道了,面试这么多人都不知道头插和尾插,仍是被你说出来了,能够能够。

面试官谬赞啊,要不是你这样美若天仙的面试官面试我,我估计是想不起来了。

我*,你套近乎?

小姐姐抿嘴一笑,小子你offer有了,耶稣都带不走你,我说的!

那我问你HashMap的默认初始化长度是多少?

我记得我在看源码的时候初始化大小是16

你那知道为啥是16么?

卧*,这叫什么问题啊?他为啥是16我怎么知道???你肯定你没逗我?

我努力回忆源码,不知道有没有漏掉什么细节,之前在学校熬夜看源码的一幕幕在脑海里闪过,想起那个晚上在操场上,跟我好了半个月的小绿拉着个人手说:你就要当爸爸了。

等等,这都是什么鬼,哦哦哦,想起来了!!!

在JDK1.8的 236 行有1<<4就是16,为啥用位运算呢?直接写16很差么?

我再次陷入沉思,疯狂脑暴,叮!

有了!

面试官您好,咱们在建立HashMap的时候,阿里巴巴规范插件会提醒咱们最好赋初值,并且最好是2的幂。

这样是为了位运算的方便,位与运算比算数计算的效率高了不少,之因此选择16,是为了服务将Key映射到index的算法。

我前面说了全部的key咱们都会拿到他的hash,可是咱们怎么尽量的获得一个均匀分布的hash呢?

是的咱们经过Key的HashCode值去作位运算。

我打个比方,key为”帅丙“的十进制为766132那二进制就是 10111011000010110100

咱们再看下index的计算公式:index = HashCode(Key) & (Length- 1)

15的的二进制是1111,那10111011000010110100 &1111 十进制就是4

之因此用位与运算效果与取模同样,性能也提升了很多!

那为啥用16不用别的呢?

由于在使用是2的幂的数字的时候,Length-1的值是全部二进制位全为1,这种状况下,index的结果等同于HashCode后几位的值。

只要输入的HashCode自己分布均匀,Hash算法的结果就是均匀的。

这是为了实现均匀分布

哟小家伙,知道的确实不少,那我问你个问题,为啥咱们重写equals方法的时候须要重写hashCode方法呢?

你能用HashMap给我举个例子么?

这都能被他问到,还好我看了敖丙的系列呀,否则真的完了!!!

可是我想拖延点时间,只能故作沉思,仰望天空片刻,45°仰望天空的样子,说实话,我看到面试官都流口水了!惋惜我是他永远得不到的男人,好了不装逼了。

我想起来了面试官!

由于在java中,全部的对象都是继承于Object类。Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。

在未重写equals方法咱们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然咱们new了2个对象内存地址确定不同

  • 对于值对象,==比较的是两个对象的值
  • 对于引用对象,比较的是两个对象的地址

你们是否还记得我说的HashMap是经过key的hashCode去寻找index的,那index同样就造成链表了,也就是说”帅丙“和”丙帅“的index均可能是2,在一个链表上的。

咱们去get的时候,他就是根据key去hash而后计算出index,找到了2,那我怎么找到具体的”帅丙“仍是”丙帅“呢?

equals!是的,因此若是咱们对equals方法进行了重写,建议必定要对hashCode方法重写,以保证相同的对象返回相同的hash值,不一样的对象返回不一样的hash值。

否则一个链表的对象,你哪里知道你要找的是哪一个,到时候发现hashCode都同样,这不是完犊子嘛。

能够能够小伙子,我记得你上面说过他是线程不安全的,那你能跟我聊聊大家是怎么处理HashMap在线程安全的场景么?

面试官,在这样的场景,咱们通常都会使用HashTable或者ConcurrentHashMap,可是由于前者的并发度的缘由基本上没啥使用场景了,因此存在线程不安全的场景咱们都使用的是ConcurrentHashMap。

HashTable我看过他的源码,很简单粗暴,直接在方法上锁,并发度很低,最多同时容许一个线程访问,ConcurrentHashMap就好不少了,1.7和1.8有较大的不一样,不过并发度都比前者好太多了。

那你能跟我聊聊ConcurrentHashMap么?

好呀,不过今每天色已晚,我以为咱们要不改天再约?

再说最近敖丙好像双十二比较忙,一次怎么能怼这么多呢?

好吧好吧,小伙子还挺会为别人着想,并且还喜欢这么优秀的做者,你我以为来日可期,那咱们改日再约,今天表现很好,但愿下次能保持住!

总结

HashMap绝对是最常问的集合之一,基本上全部点都要烂熟于心的那种,篇幅和时间的关系,我就很少介绍了,核心的点我基本上都讲到了,不过像红黑树这样的就没怎么聊了,可是不表明不重要。

篇幅和精力的缘由我就介绍到了一部分的主要知识点,我总结了一些关于HashMap常见的面试题,你们问下本身能不能回答上来,不能的话要去查清楚哟。

HashMap常见面试题:

  • HashMap的底层数据结构?

  • HashMap的存取原理?

  • Java7和Java8的区别?

  • 为啥会线程不安全?

  • 有什么线程安全的类代替么?

  • 默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

  • HashMap的扩容方式?负载因子是多少?为什是这么多?

  • HashMap的主要参数都有哪些?

  • HashMap是怎么处理hash碰撞的?

  • hash的计算规则?

点关注,不迷路

好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是人才

我后面会每周都更新几篇一线互联网大厂面试和经常使用技术栈相关的文章,很是感谢人才们能看到这里,若是这个文章写得还不错,以为「敖丙」我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对暖男我来讲真的 很是有用!!!

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!

敖丙 | 文 【原创】

若是本篇博客有任何错误,请批评指教,不胜感激 !


文章每周持续更新,能够微信搜索「 三太子敖丙 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub github.com/JavaFamily 已经收录,有一线大厂面试点思惟导图,也整理了不少个人文档,欢迎Star和完善,你们面试能够参照考点复习,但愿咱们一块儿有点东西。

相关文章
相关标签/搜索