hashMap拓展

HashMap我们谈点不一样的

课程目的

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

课程内容

1、对象的hashCode方法它的返回值是int类型——产生的疑问

Java里面Object类是所有引用类型的父类,Object中的hashCode方法可以被所有子类继承使用。

hashCode方法是用来干嘛的呢?

hashCode是程序(一套底层算法)给对象分配的一个代表它的数值,用于散列存储这个对象时有据可依。方便针对这个对象使用Java中的各种散列数据结构做存储,比如像HashMap、HashSet。

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

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

2、HashMap的综合效率是如何保证的——空间换时间

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

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

比如:n%2取模,要么是0,要么是1;

​ n%3取模,可能值就变成了0,1,2;

​ …

​ 存储同样的数据量,数组的长度越大,取模的值的可能就越多,也就代表离散更大;

这也是为什么会存在负载因子、扩容这一回事了的原因了。为了保证效率,不会让散列数组存满的。但是,有限的空间长度len,针对整个int范围的hashCode值被取模,怎么样都可能存在下标冲突,这也是哈希冲突产生的原因,由此,HashMap为解决这个问题,用链表来解决哈希冲突。但是链表的引入,多少会影响寻址的效率。

3、HashMap是如何存数、找数、寻找下标的——此算法必须领悟并记住,这就是资本

说明

1、如果key为null,则默认对应的数组下标为0;

2、key的hashCode值记为hashCode;

3、记h = hashCode ^ (hashCode >>> 16);

4、记数组长度len也就是HashMap容量cap;

5、index = h & (cap-1);

如何理解这个算法?

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

其实这个算法的前后,包括它的扩容机制、初始化长度,都是相辅相成、前后呼应的。是由此才得出的一套完整的算法解决方案。

根据最后的取模算法 h & (cap-1),如果直接使用hashCode & (cap - 1),当数组长度比较小的时候,hashCode值只有它的低位才会参与求取下标的计算。而一旦我们所需要存储的数据的key整体的hashCode值都比较大、并且低位都相同的时候,那么直接使用这个hashCode值来对数组长度取模,那产生的冲突将是非常恐怖的!所以,这个算法,在进行后面取模求index之前,先获取了一个更具有代表这个数据整体的数值h(h = hashCode ^ (hashCode >>> 16))。这样操作之后就将hashCode值的高低16位都运用了起来,这样h & (cap - 1)取模的离散算法就更优了,产生的冲突的几率更小,访问效率更高。

图解

1、如下,当直接使用hashCode来取模时,因为数组长度比较小,就可能存在多个hashCode对应一个模长。这不是一个合格的离散算法!

在这里插入图片描述
2、我们改改,也就是HashMap中的正确的取模算法,让高低16位都参与取模运算,图解如下。

上图第一个数,最终取模如下:
在这里插入图片描述

第二个数,最终取模如下:
在这里插入图片描述

…由此可见,这样操作,最终得到的下标值离散度是更好的。

4、扩容核心,为何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)、寻址算法、扩容实现的核心所在!

扩容之后,冲突位置的值是否需要移动,就看扩容位对应的h的二进制位是0还是1,是0则原位置不变,是1则移至新空间对应位置。所以这里有2分之1的可能会将原数据移动!新增一倍的空间,将原空间一半的数据移动到新空间,让整体空间重新达到离散平衡!

图解如下:(如果扩容位为0,则数据保持原下标不变!)

在这里插入图片描述

如此算法非常优秀的支持了扩容,大大提升了扩容效率,移动的元素的新的下标值都不需要从新计算,只是用原来的下标+原来的容量即可得到,移动数据量也是最少的,离散平衡也能依旧保持。

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

如果我们确切的知道我们有多少键值对需要要存储(其它的动态数据结构使用亦是如此),那么我们在初始化HashMap的时候就应该指定它的容量,以防止HashMap自动扩容,影响使用效率。

有的同学说应该给它初始化容量为1000,有的同学说应该给他初始化为1024。实则都不对!

HashMap底层是这样实现的初始化的,当你给HashMap初始化为一个n的容量时,程序会用算法自动计算出一个不小于n的一个二的次幂值(原因前面已经说明),所以这里你就算给他指定1000容量,实则初始化后分配的容量也是1024。

在这里插入图片描述

如上代码,实则是将cap这个数的二进制数从第一个1开始,把后面的二进制位全部变成1,然后+1操作,实则是取不小于n的一个最近的2的次幂。

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

6、高并发情况下扩容时的循环死链,CPU百分之百占用案例分析,以及Java8是如何避免的?

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

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

在这里插入图片描述
图解循环死链:

在这里插入图片描述
在这里插入图片描述

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

7、树形化条件分析——并不是单纯的长度大于8!

在这里插入图片描述

如上可知,树形化的条件不是单纯的链表长度大于8,还得要求是容量不能小于64,否则会用扩容手段减少链表长度。

8、你们知道有一种结构,既可以维持链表又可以维持二叉树么?——树形化后链表依旧存在且维持!

在链表转化成红黑二叉树之后,其实链表结构并没有退化,而是继续维持且更新,便于后期扩容、或数据缩减时退化成链表,也可以继续使用链表的优势,比如全局遍历…

在这里插入图片描述

9、HashMap最多能存多少键值对?为什么?

1、HashMap在确定数组下标Index的时候,采用的是( length-1) & hash的方式,只有当length为2的指数幂的时候才能较均匀的分布元素。所以HashMap规定了其容量必须是2的n次方;

2、由于HashMap规定了其容量是2的n次方,所以我们采用位运算<<来控制HashMap的大小。使用位运算同时还提高了Java的处理速度。HashMap内部由Entry[]数组构成,Java的数组下标是由Int表示的。所以对于HashMap来说其最大的容量应该是不超过int最大值的一个2的指数幂,而最接近int最大值的2的指数幂用位运算符表示就是 1 << 30;

3、初始容量问题,可以赋值容量小于16!

OVER彩蛋

教你们循序渐进、深入浅出的表达HashMap

1、散列算法。先^让高低16位都参与获取代表hash值,再&取模,得下标。

2、数组扩容位跟hash值对应位比较,0不动,1移动,重新达到离散平衡,扩容算法最优,效率最高。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值