正文
面试官:最近公司要进行招聘,每天都要面好多人(好烦),我要想个法子,过滤掉一些人,那就先用基础去试试面试者的深浅。嘿嘿嘿
小伙子,你平常工作中使用的数据结构有什么呀
平常使用比较多的是List,HashMap这些。
那你先给我讲讲HashMap呗
表面:我捋一下哈,挠了挠头(假装思考)
HashMap是我们非常常用的数据结构,由数组+链表/红黑树组成
下面是存储时候的草图(辅助大家理解)
那它是如何进行储存的呢
内心:这个简单,我可太会了(so easy)。
表面:我举个例子把(我在想笔记内容,你等等)
在我们进行put操作的时候,会把key经过计算得到Hashcode值,在通过
Hash的公式:index = HashCode(Key) & (Length - 1),计算得到index值
index值就代表这个节点Node<k,v>要放入数组的哪一个桶中。再依次与链表中的元素通过equals方法进行比较,若存在这个key,则进行覆盖操作。不存在这个key,则会进行插入操作(在不同的JDK中,使用的插入方法不同,后续下文中会介绍)。
解释下Hash公式,还有为什么不采用取余的方式(轻蔑)
内心:不慌,小场面(擦汗)
表面:为了Peace&Love(小声bb),为了让HashMap存取更加高效,尽量减少Hash碰撞。理想情况下,不同key就Hash到不同桶中,实现效果就是一个萝卜一个坑。但是这样的话,用于储存的数组空间就要足够大,够大内存也不放下,所以就会存在哈希碰撞的情况,出现碰撞就放入链表/红黑树中。同时哈希函数映射也要保证足够分散。如何保证哈希函数映射分散呢?默认的数组长度是2的幂,Length-1的值转换为2进制都是1111的形式,所以index的值,其实就是看HashCode(key)对应的后几位。这样HashCode(Key)足够分散,index值就也足够分散了。
为什么不采用取余的方式呢?眼尖的已经发现了 hash%length=hash&(length-1),那为什么不使用前者呢,因为这两者相等的前提是length的值是2的幂,同时与运算符&比取余的效率更高。
这么会,那你讲下扩容机制
内心:我不会啊。。。还好我做了笔记(大脑高速运转)
表面:我们知道默认的数组大小是16,负载因子为0.75(时间和空间的折中),HashMap中的扩容的个数是针对size(内部元素(节点)总个数),而不是数组的个数。比如说初始容量为16,第十三个节点put进来,不管前面十二个占的数组位置如何,就开始扩容。
resize操作步骤如下
- 创建一个新的数组Entry,数组大小是原来数组的2倍;
- 遍历原来的数组,把所有的元素重新Hash进新的数组。
嗯。对了,你前面说不同的JDK使用的插入方法是不一样的,你说说看
内心:真多嘴啊,说多漏多,慌了慌了。
表面:在JDK1.8以前,当发生hash碰撞,则会把元素插入链表的最前面,简称“头插法”,在JDK1.8之后,不再采用"头插法",而采用“尾插法”来插入元素,同时当链表长度大于8的时候则会将链表转换为红黑树,从而提升查询效率。
tip: 采用红黑树是因为在查找的时候,是通过Hash公式(上方),计算得到index值,再通过equels方法,与该链表的元素进行依次比较。这样的话,最坏的情况是要与链表的所有元素进行比较,复杂度为O(N),若使用是红黑树,则复杂度会大大降低,复杂度为O(logN)
那为什么要在JDK1.8的时候改用尾插法,“头插法”它不香吗?
因为爱情???
那你回去等通知把
等等,我想想,嗯嗯嗯,有了有了。
应该是因为HashMap是线程不安全的把
这关线程不安全什么事?
因为使用头插法会改变链表的顺序,当不同的线程进行resize操作时,可能会形成环状链表,这样我们再执行查询操作的时候,就可能陷入死循环。
死循环原因:链表元素一直存在下一个节点,像一个圈,永远找不到尾节点。
那要如何解决线程不安全呢
Hashtable(脱口而出),因为它内部的方法都是用synchronized修饰的。可以保证线程安全,但是效率就会低一点。(男人嘛,得到一些东西,总会失去另一些东西)
那你回去等通知把
等等,我想想,嗯嗯嗯,有了有了。
现在HashTable基本被淘汰了,不建议使用,推荐使用ConcurrentHashMap 。
那你对ConcurrentHashMap应该挺了解的吧
我不会,等二面再给你讲。
结语
HashMap作为面试中比较常见的问点,即使看不懂源码,也要懂得如何表达。
如果本篇博客有任何错误,欢迎讨论!!!