面试:HashMap 夺命二十一问!

作者 | 菜鸟小于

来源 | cnblogs.com/Young111/p/11519952.html

1:HashMap 的数据结构?

A:哈希表结构(链表散列:数组 + 链表)实现,结合数组和链表的优点。当链表长度超过 8 时,链表转换为红黑树。

transient Node<K,V>\[\] table;

2:HashMap 的工作原理?

HashMap 底层是 hash 数组和单向链表实现,数组中的每个元素都是链表,由 Node 内部类(实现 Map.Entry 接口)实现,HashMap 通过 put & get 方法存储和获取。

存储对象时,将 K/V 键值传给 put() 方法:

①、调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;

②、调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容 resize 为 2n);

③、i. 如果 K 的 hash 值在 HashMap 中不存在,则执行插入,若存在,则发生碰撞;

ii. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 true,则更新键值对;

iii. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。

(JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法)(注意:当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树)

获取对象时,将 K 传给 get() 方法:①、调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;②、顺序遍历链表,equals() 方法查找相同 Node 链表中 K 值对应的 V 值。

hashCode 是定位的,存储位置;equals 是定性的,比较两者是否相等。

3. 当两个对象的 hashCode 相同会发生什么?

因为 hashCode 相同,不一定就是相等的(equals 方法比较),所以两个对象所在数组的下标相同,"碰撞" 就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。为什么要重写 hashcode 和 equals 方法?推荐看下。

4. 你知道 hash 的实现吗?为什么要这样实现?

JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。

5. 为什么要用异或运算符?

保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。

6.HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?

①、table 数组大小是由 capacity 这个参数确定的,默认是 16,也可以构造时传入,最大限制是 1<<30;

②、loadFactor 是装载因子,主要目的是用来确认 table 数组是否需要动态扩展,默认值是 0.75,比如 table 数组大小为 16,装载因子为 0.75 时,threshold 就是 12,当 table 的实际大小超过 12 时,table 就需要动态扩容;

③、扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold

④、如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。

7.HashMap 中 put 方法的过程?

答:“调用哈希函数获取 Key 对应的 hash 值,再计算其数组下标;

如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;

如果链表长度超过阀值 (TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于 6,就把红黑树转回链表;

如果结点的 key 已经存在,则替换其 value 即可;

如果集合中的键值对大于 12,调用 resize 方法进行数组扩容。”

8. 数组扩容的过程?

创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标 + 旧数组的大小。

9. 拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。推荐:面试问红黑树,我脸都绿了。

而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持 “平衡” 是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于 8 的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

10. 说说你对红黑树的见解?

  • 每个节点非红即黑

  • 根节点总是黑色的

  • 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

  • 每个叶子节点都是黑色的空节点(NIL 节点)

  • 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

11.jdk8 中对 HashMap 做了哪些改变?

在 java 1.8 中,如果链表的长度超过了 8,那么链表将转换为红黑树。(桶的数量必须大于 64,小于 64 的时候只会扩容)

发生 hash 碰撞时,java 1.7 会在链表的头部插入,而 java 1.8 会在链表的尾部插入

在 java 1.8 中,Entry 被 Node 替代 (换了一个马甲。

12.HashMap,LinkedHashMap,TreeMap 有什么区别?

HashMap 参考其他问题;

LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;

TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)

13.HashMap & TreeMap & LinkedHashMap 使用场景?

一般情况下,使用最多的是 HashMap。

HashMap:在 Map 中插入、删除和定位元素时;

TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;

LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。

14.HashMap 和 HashTable 有什么区别?

①、HashMap 是线程不安全的,HashTable 是线程安全的;

②、由于线程安全,所以 HashTable 的效率比不上 HashMap;

③、HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null,而 HashTable 不允许;

④、HashMap 默认初始化数组的大小为 16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍 + 1;

⑤、HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

15.Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?

ConcurrentHashMap 类(是 Java 并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。

HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);

而针对 ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;JDK 1.8 中直接采用了 CAS(无锁算法)+ synchronized。

16.HashMap & ConcurrentHashMap 的区别?

除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有 null,但是 ConCurrentHashMap 都不允许。

17. 为什么 ConcurrentHashMap 比 HashTable 效率要高?

HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;

ConcurrentHashMap

  • JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。

  • JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结

    点)(实现 Map.Entry)。锁粒度降低了。

18. 针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)

JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组 + 链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。

①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;

②、HashEntry 用来封装映射表的键 - 值对;

③、每个桶是由若干个 HashEntry 对象链接起来的链表

JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。

19.ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?

①、粒度降低了;

②、JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。

③、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

20.ConcurrentHashMap 简单介绍?

①、重要的常量:

private transient volatile int sizeCtl;

当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容;

当为 0 时,表示 table 还没有初始化;

当为其他正数时,表示初始化或者下一次进行扩容的大小。

②、数据结构:

Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据;

TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据;

TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。

③、存储对象时(put() 方法):

如果没有初始化,就调用 initTable() 方法来进行初始化;

如果没有 hash 冲突就直接 CAS 无锁插入;

如果需要扩容,就先进行扩容;

如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历

到尾端插入,一种是红黑树就按照红黑树结构插入;

如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循环

如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。

④、扩容方法 transfer():默认容量为 16,扩容时,容量变为原来的两倍。

helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。

⑤、获取对象时(get() 方法):

计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;

如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find() 方法,查找该结点,匹配就返回;

以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。

21.ConcurrentHashMap 的并发度是什么?

程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。

当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17,实际并发度则为 32)

展开阅读全文

150讲轻松搞定Python网络爬虫

05-16
【为什么学爬虫?】        1、爬虫入手容易,但是深入较难,如何写出高效率的爬虫,如何写出灵活性高可扩展的爬虫都是一项技术活。另外在爬虫过程中,经常容易遇到被反爬虫,比如字体反爬、IP识别、验证码等,如何层层攻克难点拿到想要的数据,这门课程,你都能学到!        2、如果是作为一个其他行业的开发者,比如app开发,web开发,学习爬虫能让你加强对技术的认知,能够开发出更加安全的软件和网站 【课程设计】 一个完整的爬虫程序,无论大小,总体来说可以分成三个步骤,分别是: 网络请求:模拟浏览器的行为从网上抓取数据。 数据解析:将请求下来的数据进行过滤,提取我们想要的数据。 数据存储:将提取到的数据存储到硬盘或者内存中。比如用mysql数据库或者redis等。 那么本课程也是按照这几个步骤循序渐进的进行讲解,带领学生完整的掌握每个步骤的技术。另外,因为爬虫的多样性,在爬取的过程中可能会发生被反爬、效率低下等。因此我们又增加了两个章节用来提高爬虫程序的灵活性,分别是: 爬虫进阶:包括IP代理,多线程爬虫,图形验证码识别、JS加密解密、动态网页爬虫、字体反爬识别等。 Scrapy和分布式爬虫:Scrapy框架、Scrapy-redis组件、分布式爬虫等。 通过爬虫进阶的知识点我们能应付大量的反爬网站,而Scrapy框架作为一个专业的爬虫框架,使用他可以快速提高我们编写爬虫程序的效率和速度。另外如果一台机器不能满足你的需求,我们可以用分布式爬虫让多台机器帮助你快速爬取数据。   从基础爬虫到商业化应用爬虫,本套课程满足您的所有需求! 【课程服务】 专属付费社群+每周三讨论会+1v1答疑

专为程序员设计的数学课

11-11
<p> 限时福利限时福利,<span>15000+程序员的选择!</span> </p> <p> 购课后添加学习助手(微信号:csdn590),按提示消息领取编程大礼包!并获取讲师答疑服务! </p> <p> <br> </p> <p> 套餐中一共包含5门程序员必学的数学课程(共47讲) </p> <p> 课程1:《零基础入门微积分》 </p> <p> 课程2:《数理统计与概率论》 </p> <p> 课程3:《代码学习线性代数》 </p> <p> 课程4:《数据处理的最优化》 </p> <p> 课程5:《马尔可夫随机过程》 </p> <p> <br> </p> <p> 哪些人适合学习这门课程? </p> <p> 1)大学生,平时只学习了数学理论,并未接触如何应用数学解决编程题; </p> <p> 2)对算法、数据结构掌握程度薄弱的人,数学可以让你更好的理解算法、数据结构原理及应用; </p> <p> 3)看不懂大牛代码设计思想的人,因为所有的程序设计底层逻辑都是数学; </p> <p> 4)想学习新技术,如:人工智能、机器学习、深度学习等,这门课程是你的必修课程; </p> <p> 5)想修炼更好的编程内功,在遇到题时可以灵活的应用数学思维解决题。 </p> <p> <br> </p> <p> 在这门「专为程序员设计的数学课」系列课中,我们保证你能收获到这些:<br> <br> <span> </span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">①价值300元编程课程大礼包</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">②应用数学优化代码的实操方法</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">③数学理论在编程实战中的应用</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">④程序员必学的5大数学知识</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">⑤人工智能领域必修数学课</span> </p> <p> <br> 备注:此课程只讲程序员所需要的数学,即使你数学基础薄弱,也能听懂,只需要初中的数学知识就足矣。<br> <br> 如何听课? </p> <p> 1、登录CSDN学院 APP 在我的课程中进行学习; </p> <p> 2、登录CSDN学院官网。 </p> <p> <br> </p> <p> 购课后如何领取免费赠送的编程大礼包和加入答疑群? </p> <p> 购课后,添加助教微信:<span> csdn590</span>,按提示领取编程大礼包,或观看付费视频的第一节内容扫码进群答疑交流! </p> <p> <img src="https://img-bss.csdn.net/201912251155398753.jpg" alt=""> </p>
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值