最近几天,在这样的大环境下显得疲惫不堪,但是我还是写下了这篇文章,希望对任何人都有用。

HashMap是我们经常用到的数据结构,由数组和链表组成的数据结构如下图所示




java中hashmap获取健值 hashmap取数据_java中hashmap获取健值


上方是一张数组图片,数组里面每个地方都存了Key-Value这样的实例,值得注意的是在java7版本中叫Entry,而在Java8里面叫做Node(节点)。因为初始化的时候所有的位置都是Null,在执行插入操作的时候会根据hash算法把key作为健得出一个Index的值。

好比如我put(“老婆”,114)的操作,则我插入了“老婆”这个元素,这个时候会把"老婆"作为hash函数的参数,计算出插入在数组的那个位置,如果根据哈希算法计算出index=2那结果如下图所示

hash(“老婆”)=2


java中hashmap获取健值 hashmap取数据_链表_02


面试官:你刚才提到了链表,为啥有数组还需要链表,链表又是什么?

我们都知道数组的长度是有限的,在有限的数组长度内我们使用哈希,哈希本身就有概率性,当执行put("l老婆",114)和put("老公",220)这就计算了两个哈希,有几率计算出的哈希值会一样,就像上面的一样我再次哈希put("老公",220)极端情况下也会哈希到一个值上,那就只能用链表表达。

如下图。


java中hashmap获取健值 hashmap取数据_hashmap怎么取值_03


每一个节点(Node)都会保存自身的hash,key,value,以及下一个节点,这是Node的源码

数组存储的是Node,next代表下一个hash相同的元素,就形成了一个想链条一样的永远都不会断。


java中hashmap获取健值 hashmap取数据_hashmap怎么取值_04


面试官:既然说到了链表,那你说下新的Enter节点在插入的时候,是怎么插入的呢

java8之前插入的顺序是往头部插入,就是说新来的元素会取代原来的元素,原来旧的元素就顺便推到链表中去,也就是说新元素next指向的是旧的元素。但是在java8之后,都是所有的尾部插入了。

面试官:我很好奇为啥改为尾部插入了呢?

这个问题!!!还好我饱读诗书,看我细细道来!!


java中hashmap获取健值 hashmap取数据_hashmap怎么取值_05


有些人认为是java作者的随机性而为,没有杀luan用,其实暗藏玄机,首先我们看下HashMap的扩容机制:

我提到了数组的容量是有限的,数据经过多次插入,到达一定数量就会用完,所以会进行自动扩容,也就是resize的操作。

面试官:什么时候会触发resize扩容的操作?

扩容有两个因素影响:

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


java中hashmap获取健值 hashmap取数据_数组_06


如何理解?例如,当前容量为100,让你存入76个元素的时候,不会立马执行存入操作,而是每次插插入的时候都是判断是否需要进行resize,如果是那就扩容,但是HashMap也不是那么简单的阔大点容量就行了。

面试官:MashMap遇到需要扩容是是怎么扩容的?

  • 分两步走:
  • 扩容:创建一个新的Entry空数组,长度是原数组的2倍
    ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组
    面试官:为什么要重新Hash呢,直接复制过去不香么?
    卧槽这个问题提得好!有点知识盲区呀!


java中hashmap获取健值 hashmap取数据_java中hashmap获取健值_07


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

面试官:是因为长度扩大以后,Hash的规则也随之改变。

Hash的公式---> index = HashCode(Key) & (Length - 1)
原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了。
扩容前:


java中hashmap获取健值 hashmap取数据_hashmap的扩容机制_08


扩容后:


java中hashmap获取健值 hashmap取数据_数组_09


面试官:继续问题,为啥之前用头部插法,java8之后改用尾部插法了呢?

我就先举个例子吧,现在往一个容量大小为2的put两个值,负载因子是0.75是不是我们在put第二个的时候就会进行resize?
2*0.75 = 1 所以插入第二个就要resize了
现在我们要在容量为2的容器里面用不同线程插入A,B,C,假如我们在resize之前打个短点,那意味着数据都插入了但是还没resize那扩容前可能是这样的。


java中hashmap获取健值 hashmap取数据_hashmap怎么取值_10


我们可以看到链表的指向变成了这样的画风,元素A-〉B-〉C,

A的下一个指针指向B,B再指向C,

突然就变得奇怪了!!!


java中hashmap获取健值 hashmap取数据_java中hashmap获取健值_11


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

可能存在以下情况。你觉得有什么问题吗?

B指向了A


java中hashmap获取健值 hashmap取数据_hashmap怎么取值_12


一旦几个线程都调整完毕,就可能出现环形链表,这个时候如果发生取值的操作悲剧就来了。

环形链表 B-〉A-〉B


java中hashmap获取健值 hashmap取数据_hashmap怎么取值_13


面试官:不错,看来你理解的差不了。那java8的尾部插入是怎么回事?

因为在java8版本的HashMap中引入了红黑树,红黑树的引入大大的提高了HashMap的性能,原本只是O(n)的时间复杂度降低到了O(logn).

关于红黑树我在其他的专栏有专门的讲过,本篇讲的是散列表暂不涉及红黑树。

使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容的时候依然会保持链表原本的顺序,避免了链表成死环的问题。原本A-〉B,扩容之后依然能保持链表的顺序。


java中hashmap获取健值 hashmap取数据_hashmap怎么取值_14


在java7版本的HashMap中,当在多线程环境下可能会造成死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

Java 8在相同的多线程环境下不会导致死循环,因为在扩容转移之后,链表顺序保持不变,并且保持了以前节点的引用关系。

面试官:那是不是意味着java8的HashMap可以在多线程环境下运行?

就算在java8版本HashMap得以修复一部分缺陷,但是通过源码得知put/get这两个方法都没有加入同步锁,因此出现出现最极端的情况是:无法保证上一秒put的值,下一秒get的时候还是原来的值,线程安全依然无法保障。

面试官:可以,可以今天你是回答得最好的。不过HashMap初始化容量是多少?

16

面试官:知道为什么是16吗?

突然脑子像被电了一下,噢噢,都想起来了看我这记性

在1.8jdk236行中有这行1<<4

使用位运算结果为16。


java中hashmap获取健值 hashmap取数据_数组_15


面试官:为啥用位运算呢,直接写16不好吗?

我赶紧把源码从头想一边,有了。

值一般最好要填2的幂,这样做的好处在于位运算比简单的数字计算效率真的高了好多,而之所以选择16位初始大小,是为了服务将Key映射到index的算法。

我前面说了所有的key我们都会拿到他的hash,但是我们怎么尽可能的得到一个均匀分布的hash呢?
是的我们通过Key的HashCode值去做位运算,通过用位的方式也提高了不少。

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


java中hashmap获取健值 hashmap取数据_java中hashmap获取健值_16


面试官:为啥用16不用其他的值作为初始值呢?
因为在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。
只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
这是为了实现均匀分布。


面试官:用HashMap的例子说明,当我们重写equals方法的时候需要重写hashCode方法的?

因为在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都一样,这不是完犊子嘛。

面试官:你能说说在场景中如果需要用到线程安全怎么办?

在这样的场景中,我们通常使用hashtable或者current HashMap,但是由于前者的并发性,基本上没有使用场景,所以当线程不安全时,我们都使用correnthashmap解决。在correnthashmap1.8的get方法上,使用了锁的机制


java中hashmap获取健值 hashmap取数据_数组_17


我今天的这篇文章中就重点介绍了在面试中常见的题,只要考官问到HashMap那么基本没有问题。