面试造航母系列之HashMap

烟花三月,疫情肆虐,不如以往。面试途中,荆棘满满。
以下是面试实录(Q表示面试官提问,A表示回答)

Q:JAVA的集合容器可以通过哪几种方式实现?

A:实现Collection接口,实现Map接口。

Q:说说几个Map接口的实现类?

A:HashMap,LinkedHashMap,ConcurrentHashMap等

Q:HashMap的数据结构是什么样的?

A:是key-value键值对的形式,有数组加链表组成。jdk1.8以后,链表长度过长会变成红黑二叉树的结构。

Q:HashMap的键能不能为null?

A:可以为null。

Q:为什么键可以为null?

A:
在这里插入图片描述
jdk1.8以后,在对key进行hash的时候,key为null的时候返回0,进行了处理。
在这里插入图片描述
在这里插入图片描述
jdk1.8之前的时候,在put值的时候,专门用一个putForNullKey对key为null的情况进行了处理。

Q:HashMap的值能不能为null?

A:可以为null。

Q:HashMap是如何存值的?

A:put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。
put的执行流程如下:在这里插入图片描述

Q:hash冲突是什么?

A:key值不同的时候,hash值一样。

Q:HashMap是如何解决hash冲突的?

A:JDK 1.7中,进行了4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)
Q:能不能具体说说jdk1.7和jdk1.8解决hash冲突的方法嘛?
A:jdk1.7的时候:在这里插入图片描述
采用了链地址的方法解决hash冲突。一开始默认h为零。零与k的hash值进行异或处理,得到的值就是k的hash值。h此时就为k的hash值。这是一次异或运算。
后面h对他本身右移20位和右移12位后分别进行了异或运算,得到了新的h,这是第二次和第三次异或运算,和第一第二次位运算
最后,把得到的新的h值在进行了右移7位和4位后分别进行了异或操作,这是第四第五次异或操作和第三第四次位运算。
jdk1.8后,就简单了很多,key的hash值右移16位以后,与可以hash值本身进行了异或运算。

Q:这样处理的好处是什么?

A:降低了hash值重复的概率。

Q:HashMap的默认长度是多少?

A:16

Q:HashMap的默认长度有什么要求?

A:是2的n次幂。

Q:为什么是2的n次幂?

A:HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1).

Q:为什么这样能均匀分布减少碰撞呢?

A:因为2的n次方减1后写成2进制,都是1。hash值跟n个1取模的时候,会把后面的几位数都取到,比如hash值的二进制是011011,长度为16,则16减1的二进制为1111,取余后的结果是1011,如果长度是15,或者14,则达不到这个效果。

Q:HashMap的长度能不能自己定义?

A:能。

Q:如何自己定义?

A:有一个public HashMap(int initialCapacity)方法,可以自定义HashMap的长度。

Q:为啥要自定义HashMap的长度?

A:因为长度远超过16的时候,比如长度是1024,这时候HashMap要进行7次扩容操作,严重影响性能。

Q:能不能讲讲HashMap的扩容是怎么实现的?

A:分析resize的源码可知
在这里插入图片描述

Q:jdk1.7在扩容的时候,会出现什么问题?

A:逆序,环形链表死循环。

Q:能不能讲讲HashMap中的逆序和环形链表死循环是什么?

在这里插入图片描述

在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况
设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1

在多线程下执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Q:为啥jdk1.8不会出现上述两种问题?

A:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况。

Q:HashMap的 取值原理是什么?

A:先定义几个变量:
1个Node的数组 tab,两个Node对象,first,e,一个int n,一个K k;

进入方法的if判断,如果不走此if,直接返回null;
判断了如下内容,并且用 && 连接(同时满足,并且有短路)
(tab = table) != null, 只要进行过 put 操作,即满足;
(n = tab.length) > 0,要求map集合中有元素(与上一个条件不同:先put再remove,此判断不成立);
(first = tab[(n - 1) & hash]) != null,还是与put时同样的计算索引方法,!=null 代表tab数组对应索引有元素;

满足最外层的if后,再次需要分2种情况讨论;
hash值也是first的hash值,传入的key也是那个key(==直接返回true,重写了 equal后 返回true也可以)
此时,直接返回first即可;
树中还是链表中?做出不同处理
1.红黑树:直接调用getTreeNode()(JDK1.8之后的)
2.链表:通过.next() 循环获取,知道找到满足条件的key为止
最后,可以返回之前定义的 Node对象 e啦。
Q:JDK1.8之后的HashMap数据结构有什么改变?
A:当链表长度过大的时候,hashmap的链表变成了红黑二叉树。当链表长度为8,就由数组变成二叉树了。

Q:1.8以后HashMap的数据结构改变有什么好处?

A:当链表变成二叉树的时候,查询效率变高了。(效率为啥高后面章节讲)

Q:HashMap线程是不是安全的?

A:不是线程安全。

Q:为什么HashMap不是线程安全的?

A:在jdk1.7中,hashmap扩容,调用transfer函数的时候,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。具体内容上面的在扩容的时候讲过。

在jdk1.8的时候,putVal方法中判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完判断过程后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
除此之前,还有就是putVal方法代码的尾部处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到++size时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。
总而言之,jdk1.7的时候是因为当并发执行扩容操作时会造成环形链和数据丢失的情况。
jdk1.8的时候,是在并发执行put操作时会发生数据覆盖的情况。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值