你要敢问我HashMap,那我绝对让你一脸懵逼 建议收藏

Posted 所得皆惊喜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你要敢问我HashMap,那我绝对让你一脸懵逼 建议收藏相关的知识,希望对你有一定的参考价值。

文章目录

前言:
早年间,HashMap是面试场上必问的面试题,出去面试也会经常碰见。为了彰显个人学习的深度,以及领悟力。我们必须在面试的过程中,说点不一样的东西,像什么数组+链表的数据结构、Java8的链表树形化这种千篇一律的答案,已经没有任何新颖之处了,是非常平庸的答案。所以我们要说,就必须说点有意思的。在这个知识点上,必须把面试官拿下,接下来为了让大家从根源上认识HashMap,并且可以深入浅出的把它表达清楚,写出了如下的博客文章

①. hashCode方法返回值是int类型

  • ①. hashCode返回类型为int,意味着,不同的对象可能存在同样的hashCode值

  • ②. int取值范围是固定的,而对象是可以无穷多的,用有穷的数值去表示无穷的对象,那么肯定会存在重复

②. HashMap的综合效率是如何保证的-空间换时间

  • ①. 数组寻址快,但是插入、删除慢。链表寻址慢,但是插入、删除快。而哈希表(散列),刚好介于二者之间,综合性能比较给力

  • ②. HashMap底层也是散列,通过key的hashCode值,经过一个计算(取模),可以迅速定位到数据的存储位置。但是,散列说到底,也是数组,只是一个空间比较富裕的数组而已,一个数据被存储到这个数组上,一般是用hashCode值对这个数组长度取模,得到数组下标的位置,从而快速定位。想要同一个位置冲突的几率小,那么只有增大空间,让离散度变大,这样效率才可保证!这也就是程序届有名的"空间换时间"的概念

③.HashMap算法

  • ①. HashMap是如何存数、找数、寻找下标的——此算法必须领悟并记住,这就是资本
  1. 如果key为null,则默认对应的数组下标为0
  2. key的hashCode值记为hashCode
  3. 记h = hashCode ^ (hashCode >>> 16)
  4. 记数组长度len也就是HashMap容量cap
  5. index = h & (cap-1)

①. h = hashCode ^ (hashCode >>> 16)的理解

  • ①. hashCode >>> 16:对hashCode值向右移16位,hashCode的值是一个32位的int,如果hashCode值比较小,那么高位就是0,相当于没有高位;如果hashCode的值比较大的话,高16就有数据

  • ②. hashCode ^ (hashCode >>> 16):这个时候,hashCode与高16位进行^运算。高16位参与运算,可以大大提高离散的程度。

②. 为何不直接使用hashCode值来跟数组长度取模?

  • ①. 如果直接使用hashCode和数组长度-1 进行运与运算,数组长度比较小的时候,这里我们使用数组长度为16。当hashCode很大的时候,高16位一直变动,而底16位处于不变的时候。因为数组长度16对应的后4位是1111。而我们hashCode低16位固定。固定的低16位和数组的后4位1111是一个固定的值,就是说元素储存的索引是固定的。元素都储存在同一个位置上,那产生的冲突将是非常恐怖的。意味着我们通过get( )去获取值,查询性能非常低,效率就也低

  • ②. 所以,这个算法,在进行后面取模求index之前,先获取了一个更具有代表这个数据整体的数值h(h = hashCode ^ (hashCode >>> 16))。这样操作之后就将hashCode值的高低16位都运用了起来,这样h & (cap - 1)取模的离散算法就更优了,产生的冲突的几率更小,访问效率更高

③. 为什么index = h & (length-1) 要减去1?

  • ①. 假如不减去1,length长度=16,这个时候和hashCode进行运与运算。16的后5位是10000,这个时候hashCode的后四位无论是什么结果都是4个0,而倒数第五位只有两个值,一个是0一个是1。如果是0那么对应储存的位置就是0;如果是1,对应储存的元素位置就是16。这个时候如果是0,那么所有的元素都储存在0位置,导致hash冲突,通过get()去查询元素时,效率极其低下。如果是1,对应储存的元素位置就是16,而索引最多只有15,所以会导致下标越界。

  • ②. 如果index = h & (length-1) ,假如这个时候length=16,我们的后4位就是1111,这个时候hashCode的后四位无论怎么变化,h & (length-1) 运算的结果都是在[0-15]之间的。

④. hashMap的二倍扩容

  • ①. 扩容核心,为何2倍扩容,为何默认初始化容量16?——新增一倍的空间,将原空间一半的数据移动到新空间,重新达到离散平衡、保证效率

二倍扩容说明:
二倍扩容、2的次幂——16初始化容量,实则都是为了完美整合寻址散列算法。
我们发现一个有趣的二进制计算技巧
n % m —> 当m的值是二的次幂的时候,它可以由 n & (m-1)替代,后者的效率也是更高。
比如:10 % 4 = 2 ==> 10 & 3 = 2

所以,如果默认初始化是16,那么数组最大下标用二进制表示就是1111。
那么当根据key获取到h值之后,h & 1111运算,获取到一个下标值,实则只跟h值的低四位有关。
关键点就在这里!当二倍扩容时!最大数组下标由二进制1111变成11111,四位变五位
那么h值二进制的第五位,它不是0,就是1!
如果是0,则下标值不变,因为0 & 1 为 0;如果是1,则新下标值=原下标值+原容量大小值
这就是二倍扩容、二的次幂默认初始化大小(16)、寻址算法、扩容实现的核心所在!

⑤. 这有1000个非重复的键值对要存储,我需要怎么初始化我的HashMap?

  • ①. 然而,1000个数你用1024的空间来存储,你还怎么实现空间换时间?富余的空间太少,哈希冲突肯定很多,影响使用效率。再说,HashMap默认负载因子0.75,它也不会让你1024空间存够1000,在存储的数据size到达768的时候,这个HashMap已经执行自动扩容了!为了不让它自动扩容,所以我们初始化为2048!当然你可以初始化为1023——2048之间的任何容量值,结果都是2048

解释:
1.如果我们确切的知道我们有多少键值对需要要存储(其它的动态数据结构使用亦是如此),那么我们在初始化HashMap的时候就应该指定它的容量,以防止HashMap自动扩容,影响使用效率。
2.有的同学说应该给它初始化容量为1000,有的同学说应该给他初始化为1024。实则都不对!
3.HashMap底层是这样实现的初始化的,当你给HashMap初始化为一个n的容量时,程序会用算法自动计算出一个不小于n的一个二的次幂值(原因前面已经说明),所以这里你就算给他指定1000容量,实则初始化后分配的容量也是1024。
如上代码,实则是将cap这个数的二进制数从第一个1开始,把后面的二进制位全部变成1,然后+1操作,实则是取不小于n的一个最近的2的次幂

⑥. 高并发情况下扩容时的循环死链,Java8如何避免

  • ①. HashMap本身就是线程不安全的,所以这种问题(循环死链)是在错误的使用方法上出现的更为严峻的问题

  • ②. 在并发扩容的情况下,Jdk7是遍历每一个数组下标位置的链表,然后逐个元素采用头插法添加到新的空间。这样操作,在并发的情况下就会出现循环死链!

  • ③. 图解循环死链:

  • ④. Jdk8采用方法内部局部变量维护这个将被移动的数据(可能是一个新链表),然后在遍历结束后统一将新链表转移至对应新的下标位置,并非Jdk7那样一个一个元素进行转移。Jdk8链表添加元素采用尾插法

⑦. 什么是 hash,什么是hash表?

1.什么是hash? 
  它是将一个任2意长度的二进制值通过一个映射关系转换成一个固定长度的二进制值
   (1).任意长度的二进制值
   (2).映射关系[哈希算法--就相当于一个大学里面的学号的一个映射规则]
   (3).固定的二进制值 [哈希值--就相当于大学里面的学号]
   任意长度的二进制值和固定长度的二进制的值 是一个一一对应的关系
   固定长度的二进制值就相当于一个关键字key
   真正有小的数据,就是这个学院的基本信息,一个任意长度的二进制值 value 
   hash知识确定了一个key和一个value的唯一映射关系
2.hash表
   (1).特点:最重要的特点---它的储存效率很高,取数据的时间复杂度是1 o(1)
   (2).hash表 是通过一个key一个输入,通过一个哈希函数,找到数组中与这个key唯一映射的value
   通过这个hash函数,找到数组中这个value的下标,找到这个value[重要这句话]
   table aa=[];
   int index=hash(key);
   int value=aa[index];  
   注意:如果我们要找8,我们先写一个for循环,8和数组中的值进行比较。这样它的时间复杂度是n
3.hash函数
   key,找下标,有哪些方法可以找到下标?
   1.除留取余数法[就是%运算]
   int  index=key % m;
   m的取值规则是什么呢?
     m要取比数组长度小的最大质数
     m=15
     :int 1=1%15; 这个1就是下标  
     key=1,value=23 储存在index=1
     key=17 value=22 储存在index=2
     key=16 value=44 储存在index=1

以上是关于你要敢问我HashMap,那我绝对让你一脸懵逼 建议收藏的主要内容,如果未能解决你的问题,请参考以下文章

面试官:谈谈分布式一致性机制,我一脸懵逼。。

一脸懵逼学习Hive的使用以及常用语法

面试被问“红黑树”,我一脸懵逼......

面试官:CPU 是如何工作的?我一脸懵逼。。

CodeForces 122G Lucky Array(一脸懵逼的树状数组)

一脸懵逼学习Hive的安装