bat大厂面试官必问的hashmap相关面试题及部分源码分析

hanease hanease     2022-11-30     142

关键词:

BAT大厂面试官必问的HashMap相关面试题及部分源码分析
0 引言
1 JDK8 中的 HashMap与 JDK7 的 HashMap 有什么不一样?
2 HashMap 中 put 方法流程
3 HashMap 的 get 方法流程
4 HashMap 扩容流程是怎样的?
5 谈谈你对红黑树的理解
为什么 HashMap 的数组的大小是2的幂次方数?
参考文章
0 引言
HashMap的相关面试题一直是BAT大厂面试官的高频面试题,笔者最近接到的阿里和开源中国面试官的面试题中都有问到有关HashMap底层实现的一些面试题,当时回答的不是很好。于是抽个时间来捋一捋HashMap的相关面试题并分析其中的部分源码,希望对我的读者粉丝们也会有一定的帮助。

1 JDK8 中的 HashMap与 JDK7 的 HashMap 有什么不一样?
JDK8中新增了红黑树,JDK8是通过数组+链表+红黑树来实现的
JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
JDK8中的因为使用了红黑树保证了插入和查询了效率,所以实际上JDK8中的Hash算法实现的复杂度降低了
JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否查过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值
JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容

2 HashMap 中 put 方法流程
在IDEA中我们找到HashMap源码中的put方法源码:

public V put(K key, V value)
return putVal(hash(key), key, value, false, true);

1
2
3
put方法调用了putVal方法,继续找到putVal方法的源码:

/**
* Implements Map.put and related methods
* @param hash key的哈希值
* @param key key值
* @param value 需要Put进去的value
* @param onlyIfAbsent 如果为true,不改变已存在的值
* @param evict 如果为false,则table 为creation 模式.
* @return 返回之前的值, 不存在则返回nullh
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
Node<K,V>[] tab; Node<K,V> p; int n, i;
//先判断table是否为空或者长度为零
if ((tab = table) == null || (n = tab.length) == 0)
//调用resize方法初始化数组
n = (tab = resize()).length;
//计算数组下标i=(tab.length - 1) & hash
if ((p = tab[i = (n - 1) & hash]) == null)
//如果数组下标处元素为空,则将hash, key, value等值封装成链表后保存在此数组下标处
tab[i] = newNode(hash, key, value, null);
else
//数组下标处元素不为空
Node<K,V> e; K k;
//若p = tab[i = (n - 1) & hash]的hash值等于入参hash同时其key值等于入参key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将tab[i = (n - 1) & hash]赋值给局部变量e
e = p;
//若数组下标处的元素是一棵红黑树
elseif (p instanceof TreeNode)
//调用TreeNode#putTreeVal方法并将其返回值赋值给e,putTreeVal方法后面再讲
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else //数组下标元素不同时满足其hash值等于入参hash和其key值等于入参key,
//同时它也不是红黑树节点,此时它仍然是Node类型的节点
for (int binCount = 0; ; ++binCount)
if ((e = p.next) == null) //若p的下一个节点为空,并把p.next赋值给e
//封装hash, key, value为一个新的Node节点赋值给p.next
p.next = newNode(hash, key, value, null);
//若binCount大于等于TREEIFY_THRESHOLD - 1
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//调用treeifyBin方法后跳出循环
treeifyBin(tab, hash);
break;

//若e的hash值等于入参hash,同时e的key值等于入参key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//跳出循环
break;
//以上两种情况都没出现时将e赋值给p
p = e;


if (e != null) // 若对应的key值在HashMap中存在映射值
//将e的value值赋值给oldValue
V oldValue = e.value;
//若onlyIfAbsent==false或者oldValue == null
if (!onlyIfAbsent || oldValue == null)
//将新值value赋值给e.value
e.value = value;
//调用afterNodeAcces方法
afterNodeAccess(e);
//返回旧值
return oldValue;


//对应的key值在HashMap中不存在映射值,属于新增一个元素,modCount值加1
++modCount;
if (++size > threshold)
//若size+1后大于阈值threshold,则调用resize方法扩容
resize();
//调用afterNodeInsertion方法
afterNodeInsertion(evict);
//返回null
return null;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
TreeNode#putTreeVal方法源码:

/**
* Tree version of putVal.
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v)
Class<?> kc = null;
boolean searched = false;
//若当前TreeNode的父节点不为null则调用root()方法获得node节点,
//否则当前TreeNode即为root节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//从红黑树的root节点开始遍历
for (TreeNode<K,V> p = root;;)
int dir, ph; K pk;
//若p节点的hash值大于入参hash值h则dir赋值为-1
if ((ph = p.hash) > h)
dir = -1;
//若p节点的hash值小于入参hash值h则dir赋值为1
elseif (ph < h)
dir = 1;
//p.key赋值给pk
elseif ((pk = p.key) == k || (k != null && k.equals(pk)))
//若满足(pk = p.key) == k或者k != null && k.equals(pk)
//则返回p节点
return p;
//若满足kc为null同时调用comparableClassFor(k)方法的返回值赋给kc后的值也为null
//或者调用compareComparables(kc, k, pk)方法后的返回值赋值给dir后等于0
elseif ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
if (!searched)
//若searched==false
TreeNode<K,V> q, ch;
searched = true;
//p.left或p.right赋值给ch后不为null,同时满足调用ch.find(h, k, kc)方法后的
//返回值赋值给q后也不为null则返回q节点
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;

//调用tieBreakOrder(k, pk)方法的返回值赋值给dir
dir = tieBreakOrder(k, pk);

//定义红黑树节点xp,并将p赋值给xp
TreeNode<K,V> xp = p;
//若dir <= 0则将p.left赋值给p,否则将p.right赋值给p,以进行下一次遍历
if ((p = (dir <= 0) ? p.left : p.right) == null)
//p节点为null走以下逻辑
//定义xpn节点,并将xp.next节点赋值给xpn
Node<K,V> xpn = xp.next;
//定义x节点为调用map.newTreeNode(h, k, v, xpn)方法后的返回值
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//若dir<=0 则将x节点赋值给xp的左孩子节点,否则将x节点赋值给xp的右孩子节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//将x节点赋值给xp的next节点
xp.next = x;
//将xp节点同时赋值给x的prev节点和parent节点
x.parent = x.prev = xp;
if (xpn != null)
//若xpn节点不为null则将x节点赋值给xpn的prev节点
((TreeNode<K,V>)xpn).prev = x;
//确保root节点是根节点,里面调用了插入节点后的平衡红黑树方法balanceInsertion(root, x)
moveRootToFront(tab, balanceInsertion(root, x));
//返回null
return null;



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
以上涉及到红黑树中的复杂算法待自己搞明白了红黑树数据结构再另外撰写一篇文章发布

综上,JDK8中HashMap的put操作流程如下:

1) 对Key求Hash值,然后再计算下标:

2)如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)
3)如果碰撞了,以链表的方式链接到后面

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

5)如果节点已经存在就替换旧值

6)如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

3 HashMap 的 get 方法流程
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

 

4 HashMap 扩容流程是怎样的?
1)HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新数组上来,这样才是数组的扩容

2)在HashMap中也是一样,先新建一个2被数组大小的数组

3)然后遍历老数组上的每一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去

4)在这个过程中就需要遍历链表,当然jdk7,和jdk8在实现时是不一样的,jdk7就是简单的遍历链表上的每一个元素,然后按每个元素的hashcode结合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率

5)而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置

6)元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收到垃圾收集器中

5 谈谈你对红黑树的理解

1)每个节点非红即黑

2)根节点总是黑色的

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

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

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

为什么 HashMap 的数组的大小是2的幂次方数?
当某个key-value对需要存储到数组中时,需要先生成一个数组下标index,并且这个index不能越界。

在HashMap中,先得到key的hashcode,hashcode是一个数字,然后通过hashcode & (table.length - 1) 运算得到一个数组下标index,是通过位运算中的与运算计算出来一个数组下标的,而不是通过取余,与运算比取余运算速度更快,但是也有一个前提条件,那就是数组的长度必须是一个2的幂次方数。
————————————————
版权声明:本文为CSDN博主「heshengfu1211」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/heshengfu1211/article/details/114051745

面试官必问的3道mq面试题,还有谁不会??

...,所以自己想巩固一下自己的技术,想了解一些面试比较容易加分的项,近期准备深入研究一下Redis和MQ这两样,这总体上都是为了解决服务器并发的原因,刚翻到了一篇有关于MQ的, 查看详情

hashmap面试必问的数据结构相关知识总结

1:HashMap的数据结构?A:哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。当链表长度超过 8 时,链表转换为红黑树。transientNode<K,V>[]table;2:HashMap的工作原理?  HashMap底层是 hash数组和单向... 查看详情

80%大厂必问的面试题,附答案

面向对象的三个特征封装,继承,多态多态的好处,代码中如何实现多态,虚拟机中如何实现多态允许不同类对象对同一消息作出相应,好处如下:可替换性:多态对已存在的代码具有可替换性可扩充... 查看详情

android大厂面试必问的androidframework如何学习,如何深入了解framework层?

前言对于大多数Android开发工程师来说,掌握AndroidFramework一定是一个不光要熟练而且还要精通的技能。高级Android工程师岗位的一些技术面试也离不开Framework。一般会针对下面几个面试题进行提问:1.Android中多进程通信的... 查看详情

阿里面试必问的dubbo相关问题

在过去持续分享的几十期阿里Java面试题中,几乎每次都会问到Dubbo相关问题,比如:“如何从0到1设计一个Dubbo的RPC框架”,这个问题主要考察以下几个方面:你对RPC框架的底层原理掌握程度。以及考验你的整体RPC框架系统设计... 查看详情

面试官必问:说说springbean的实例化过程?

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇作者:厚朴链接:https://juejin.cn/post/6929672218322731022/来源:稀土掘金不贴代码,Spring的Bean实例化过程应该是怎样的?两个阶段容器启... 查看详情

java面试必问的hashmap,java工程师视频教程

5节创建者模式第1节:工厂方法模式第2节:抽象工厂模式第3节:建造者模式第4节:原型模式第5节:单例模式7节结构型模式第1节:适配器模式第2节:桥接模式第3节:组合模式第4节:装饰器模... 查看详情

handler面试必问八大题:如何深挖原理进大厂?1万+字带你详细剖析!(代码片段)

Handler一直是面试过程中的常客,我们今天来看看围绕Handler究竟能玩出那些花儿来。Handler机制几乎是Android面试时必问的问题,虽然看过很多次handler源码,但是有些面试官问的问题却不一定能够回答出来,趁着机... 查看详情

面试官必问java并发知识总结-同步与锁(代码片段)

同步对象与锁 什么叫java同步?就是java用来保证多线程在共享的内存或临界区,能够可以按照可以预期的行为去顺序执行。如果不做这个同步,那么线程的行为就不可以预期。解释这个问题最好的就是给个实践的例... 查看详情

大厂面试必问!hashmap怎样解决hash冲突?(代码片段)

HashMap冲突解决方法比较考验一个开发者解决问题的能力。下文给出HashMap冲突的解决方法以及原理分析,无论是在面试问答或者实际使用中,应该都会有所帮助。在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模... 查看详情

面试必问的redis:数据结构和基础概念

...中出现频率比redis高的也不多,可能就那么几个:HashMap、线程池之类的。 由于比较重要, 查看详情

jvm面试必问的cms,你懂了吗?(代码片段)

...司的生产环境主要使用的CMS,包括我自己呆过的几家大厂也是。因此在JVM面试中,CMS也是问的最多的,包括我自己现在面试别人时,问到JVM这一块,我也喜欢从CMS开始,逐 查看详情

jvm面试必问的cms,你懂了吗?(代码片段)

...司的生产环境主要使用的CMS,包括我自己呆过的几家大厂也是。因此在JVM面试中,CMS也是问的最多的,包括我自己现在面试别人时,问到JVM这一块,我也喜欢从CMS开始,逐 查看详情

字节软测面试必问的selenium自动化测试框架设计,你会了吗?

...、跳槽的问题。不熟悉自动化测试,也没接触过主流大厂技术,之前在小公司做点工,现在想进大厂拿高薪,该怎么做?类似上述的问题是最 查看详情

android大厂面试必问的androidframework如何学习,如何深入了解framework层?

前言对于大多数Android开发工程师来说,掌握AndroidFramework一定是一个不光要熟练而且还要精通的技能。高级Android工程师岗位的一些技术面试也离不开Framework。一般会针对下面几个面试题进行提问:1.Android中多进程通信的... 查看详情

跳槽进字节跳动了,面试真的很简单

...#xff0c;相信很多小伙伴都在面试找工作,怎样才能拿到大厂的offer,没有掌握绝对的技术,那么就要不断的学习如何拿下阿里等大厂的offer的呢,今天分享一个秘密武器,资深测试工程师整理的软件测试面试核心... 查看详情

测试需要掌握的数据库sql知识:面试中sql相关必问的问题:连接查询和索引(代码片段)

一、前言未看过文章(一)的朋友,需要准备测试数据测试数据sql如下:SETNAMESutf8mb4;SETFOREIGN_KEY_CHECKS=0;--------------------------------Tablestructureforclass------------------------------DROPTABLEIFEXISTS`cl 查看详情

面试必问的hashcode技术内幕

hashCode的内幕tips:面试常问/常用/常出错hashCode到底是什么?是不是对象的内存地址?1)直接用内存地址?目标:通过一个Demo验证这个hasCode到底是不是内存地址publicnativeinthashCode();com.hashcode.HashCodeTestpackagecom.hashcode;importorg.openjdk.jo... 查看详情