众所周知,在面试大厂的过程中,HashMap源码是必问的,当然有些还会问TreeMap 和 LinkedHashMap。如果事先没有准备,当然会瞬间懵逼,因此在面试前建议大家抽出时间准备准备,很容易就能续写自己的高光时刻。

在开始之前,强烈推荐阅读这个多达两万字的HashMap源码分析文章,是的,你想要的,它都有!强化基础,HashMap源码全角度详细解析
别的废话就不多说了,下面开始见招拆招。

本文目录:

  • 1、说一说 HashMap 底层数据结构
  • 2、那你说一下Map的hash算法
    • 1. 为什么不用 key对数组大小取余,而是需要用 key 的 hash 值对数组大小取余?
    • 2. 计算 hash 值时,为什么需要右移 16 位?
    • 3. 为什么把取模操作换成了 & 操作?
    • 4. 为什么提倡数组大小是 2 的幂次方?
  • 3、HashMap、TreeMap、LinkedHashMap有什么相同点和不同点?
    • 3.1、首先是相同点:
    • 3.2、其次是不同点:
  • 4、HashMap源码里面是怎么解决hash冲突的?
  • 5、那你说说HashMap是怎么扩容的?
  • 6、那为什么链表个数大于等于8时,链表要转化成红黑树了?为什么不是6,不是10?
  • 7、红黑树什么时候转变成链表?
  • 8、HashMap 在 put 时,如果数组中已经有了这个 key,我不想把 value 覆盖怎么办?取值时,如果得到的 value 是空时,想返回默认值怎么办?
  • 9、删除数据时可以使用foreach进行删除吗?
  • 10、LinkedHashMap中的LRU是怎么实现的?
  • 11、为什么使用TreeMap时推荐把key实现 Comparable 接口?什么情况下不需要去实现这个接口呢?

1、说一说 HashMap 底层数据结构

HashMap 底层是数组 + 链表 + 红黑树的数据结构,数组的主要作用是方便快速查找,时间复杂度是 O(1),默认大小是16,数组的下标索引是通过 key 的 hashcode 计算出来的,数组元素叫做 Node,当多个 key 的 hashcode 一致,但 key 值不同时,单个 Node 就会转化成链表,链表的查询复杂度是 O(n),当链表的长度大于等于 8 并且数组的大小超过 64 时,链表就会转化成红黑树,红黑树的查询复杂度是 O(log(n)),简单来说,最坏的查询次数相当于红黑树的最大深度。

2、那你说一下Map的hash算法

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以得出key在数组中的位置公式:tab[(n - 1) & hash]

如上代码是 HashMap 的hash 算法。

这其实可以理解为一个数学问题,源码中就是通过以上代码来计算 hash 的,首先计算出 key 的 hashcode,因为 key 是 Object,所以会根据 key 的不同类型进行 hashcode 的计算,接着计算h ^ (h >>> 16) ,这么做的好处是使大多数场景下,算出来的 hash 值比较分散。

一般来说,hash 值算出来之后,要计算当前 key 在数组中的索引下标位置时,可以采用取模的方式,就是索引下标位置 = hash 值 % 数组大小,这样做的好处,就是可以保证计算出来的索引下标值可以均匀的分布在数组的各个索引位置上,但取模操作对于处理器的计算是比较慢的,数学上有个公式,当 b 是 2 的幂次方时,a % b = a &(b-1),所以此处索引位置的计算公式我们可以更换为: (n-1) & hash。

当你顺利的答出了上面这个问题后,可能面试官会接着追问如下四个问题,别怕,接着说就好了!

1. 为什么不用 key对数组大小取余,而是需要用 key 的 hash 值对数组大小取余?

如果 key 是数字,直接用 key % 数组大小是完全没有问题的,但我们的 key 还有可能是字符串,是复杂对象,这时候用 字符串或复杂对象 % 数组大小是不行的,所以需要先计算出 key 的 hash 值。

2. 计算 hash 值时,为什么需要右移 16 位?

hash 算法是 h ^ (h >>> 16),为了使计算出的 hash 值更分散,所以选择先将 h 无符号右移 16 位,然后再于 h 异或时,就能达到 h 的高 16 位和低 16 位都能参与计算,减少了碰撞的可能性。

3. 为什么把取模操作换成了 & 操作?

key.hashCode() 算出来的 hash 值还不是数组的索引下标,为了随机的计算出索引的下表位置,我们还会用 hash 值和数组大小进行取模,这样子计算出来的索引下标比较均匀分布。

取模操作处理器计算比较慢,处理器对 & 操作就比较擅长,换成了 & 操作,是有数学上证明的支撑,为了提高了处理器处理的速度。

4. 为什么提倡数组大小是 2 的幂次方?

因为只有大小是 2 的幂次方时,才能使 hash 值 % n(数组大小) == (n-1) & hash 公式成立。

3、HashMap、TreeMap、LinkedHashMap有什么相同点和不同点?

3.1、首先是相同点:

三者在特定的情况下,都会使用红黑树;
底层的 hash 算法相同;
在迭代的过程中,如果 Map 的数据结构被改动,都会报 ConcurrentModificationException 的错误。

3.2、其次是不同点:

HashMap数据结构以数组为主,查询非常快,TreeMap 数据结构以红黑树为主,利用了红黑树左小右大的特点,可以实现 key 的排序,LinkedHashMap 在 HashMap 的基础上增加了链表的结构,实现了插入顺序访问和最少访问删除两种策略;
由于三种 Map 底层数据结构的差别,导致了三者的使用场景的不同:

  • TreeMap 适合需要根据 key 进行排序的场景;
  • LinkedHashMap 适合按照插入顺序访问,或需要删除最少访问元素的场景;
  • 剩余场景我们使用 HashMap 即可,我们工作中大部分场景基本都在使用 HashMap。

4、HashMap源码里面是怎么解决hash冲突的?

hash 冲突指的是 key 值的 hashcode 计算相同,但 key 值不同的情况。
通过学习源码,可以知道如果桶中元素原本只有一个或已经是链表了,新增元素直接追加到链表尾部;如果桶中元素已经是链表,并且链表个数大于等于 8 时,此时有两种情况:

  • 如果此时数组大小小于 64,数组再次扩容,链表不会转化成红黑树;
  • 如果数组大小大于 64 时,链表就会转化成红黑树。

这里不仅仅判断链表个数大于等于 8,还判断了数组大小,数组容量小于 64 没有立即转化的原因,猜测主要是因为红黑树占用的空间比链表大很多,转化也比较耗时,所以数组容量小的情况下冲突严重,可以先尝试扩容,通过扩容看看能不能解决根本的问题。

5、那你说说HashMap是怎么扩容的?

扩容需要满足一定的条件,主要是两种:

  1. put 时,发现数组为空,进行初始化扩容,默认扩容大小为 16;
  2. put 成功后,发现现有数组大小大于扩容的门阀值时,进行扩容,扩容为老数组大小的 2 倍。

其次扩容的门阀是 threshold,每次扩容时 threshold 都会被重新计算,门阀值等于数组的大小 * 影响因子(0.75)。新数组初始化之后,需要将老数组的值拷贝到新数组上,链表和红黑树都有自己拷贝的方法。

6、那为什么链表个数大于等于8时,链表要转化成红黑树了?为什么不是6,不是10?

HashMap在源码的类注释里对为什么是8有过解释,是这样说的:通过泊松分布公式计算,正常情况下,链表个数出现 8 的概念不到千万分之一,所以说正常情况下,链表都不会转化成红黑树,这样设计的目的,是为了防止非正常情况下,比如 hash 算法出了问题时,导致链表个数轻易大于等于 8 时,仍然能够快速遍历。
虽然成红黑树,可以使遍历的时间复杂度降低,但是也要考虑到转化成红黑树,有空间和转化耗时的成本,因此不能轻易的转化。

7、红黑树什么时候转变成链表?

当节点的个数小于等于 6 时,红黑树会自动转化成链表,主要还是考虑红黑树的空间成本问题,当节点个数小于等于 6 时,遍历链表也很快,所以红黑树会重新变成链表。

8、HashMap 在 put 时,如果数组中已经有了这个 key,我不想把 value 覆盖怎么办?取值时,如果得到的 value 是空时,想返回默认值怎么办?

如果数组有了 key,但不想覆盖 value ,可以选择 putIfAbsent 方法,这个方法有个内置变量onlyIfAbsent,内置是 true ,就不会覆盖,我们平时使用的 put 方法,内置 onlyIfAbsent 为 false,是允许覆盖的。
取值时,如果为空,想返回默认值,可以使用 getOrDefault 方法,方法第一参数为 key,第二个参数为你想返回的默认值,如 map.getOrDefault(“2”,“0”),当 map 中没有 key 为 2 的值时,会默认返回 0,而不是空。

9、删除数据时可以使用foreach进行删除吗?

不可以,因为在for循环之前modCount会赋值给mc,但是循环之后,mouCount发生了变化,但是mc却是变化之前的值,这点和ArrayList删除数据类似,都是不可以用简单的foreach删除的。

10、LinkedHashMap中的LRU是怎么实现的?

首先LRU的英文全称是Least recently used,翻译过来就是最近最少访问,在 LinkedHashMap 中,也叫做最少访问删除策略,我们可以通过 removeEldestEntry 方法设定一定的策略,使最少被访问的元素,在适当的时机被删除,原理是在 put 方法执行的最后,LinkedHashMap 会去检查这种策略,如果满足策略,就删除头节点。

保证头节点就是最少访问的元素的原理是:LinkedHashMap 在 get 的时候,都会把当前访问的节点,移动到链表的尾部,慢慢的,就会使头部的节点都是最少被访问的元素了。

11、为什么使用TreeMap时推荐把key实现 Comparable 接口?什么情况下不需要去实现这个接口呢?

这是因为 TreeMap 的底层就是通过排序来比较两个 key 的大小的,所以推荐 key 实现 Comparable 接口,是为了往你希望的排序顺序上发展。
如果我们的key的类型本身已经实现了 Comparable 接口,比如String以及一些包装类型如Long、Double、Short 等等就不需要我们再去实现 Comparable 接口了。

别怕,遇到Map源码面试题这样答就对了相关推荐

  1. 面试官系统精讲Java源码及大厂真题 - 17 并发 List、Map源码面试题

    17 并发 List.Map源码面试题 梦想只要能持久,就能成为现实.我们不就是生活在梦想中的吗? 引导语 并发 List 和 Map 是技术面时常问的问题,问的问题也都比较深入,有很多问题都是面试官 ...

  2. 面试官系统精讲Java源码及大厂真题 - 10 Map源码会问哪些面试题

    10 Map源码会问哪些面试题 更新时间:2019-09-10 10:34:08 人的一生可能燃烧也可能腐朽,我不能腐朽,我愿意燃烧起来! --奥斯特洛夫斯基 引导语 Map 在面试中,占据了很大一部 ...

  3. Android 面试准备进行曲 (Handler源码/面试题)v1.3

    前言 Handler 流程 流程源码 Handler 初始化 Looper 初始化 Handler 发送消息 Looper.loop -> MessageQueue.next Handler 消 ...

  4. 源码解读_Go Map源码解读之Map迭代

    点击上方蓝色"后端开发杂谈"关注我们, 专注于后端日常开发技术分享 map 迭代 本文主要是针对map迭代部分的源码分析, 可能篇幅有些过长,且全部是代码, 请耐心阅读. 源码位置 ...

  5. go语言api源码中文版_Go语言学习——sync.map源码剖析

    1.简介 最近看了下Sync包,详读了sync.map源码,感觉源码实现还是比较巧妙的,有不少可以学习的地方:在讲源码前,先看下sync.map的"历史",从网上搜资料,sync. ...

  6. 【Golang 源码】sync.Map 源码详解

    sync.Map 不安全的 map go 中原生的 map 不是并发安全的,多个 goroutine 并发地去操作一个 map 会抛出一个 panic package main import &quo ...

  7. 面试官系统精讲Java源码及大厂真题 - 38 线程池源码面试题

    38 线程池源码面试题 与有肝胆人共事,从无字句处读书. --周恩来 引导语 线程池在日常面试中占比很大,主要是因为线程池内容涉及的知识点较广,比如涉及到队列.线程.锁等等,所以很多面试官喜欢把线程池 ...

  8. Golang map源码详解

    Golang的map是用哈希表实现的,在实现性能上非常优秀,这里会主要对map创建.插入.查询.删除以及删除全部的源码做详解,刻意避开了扩容以及迭代相关的代码,后续会用一个新的文章去讲述.Golang ...

  9. golang1.15.6 版本 map 源码笔记

    当前我们基于golang 1.15.6 版本源码进行解析.关于map 的图形化记录大家可以查看惜暮大神的Golang map实践以及实现原理.我在这里就不在赘述了.接下来的部分我会重点关注map 源码 ...

最新文章

  1. MailBee.NET Objects撰写邮件教程(一):添加和删除自定义标题
  2. obj,lib,dll,exe
  3. VC6.0显示代码行号
  4. 青光眼-复内路粘小管成形术(ABiC)-转载
  5. Why product sales area is not replicated to CRM
  6. android学习笔记:数据库存储
  7. Golang——error处理及panic、recover使用的正确姿势
  8. Spring事务—方法调用事务回滚
  9. Golang 删除切片指定元素
  10. appium连接真机时,报错:error: device unauthorized.
  11. J2EE6 servlet session超时机制
  12. 既往不恋,当下不杂,未来不迎
  13. java 图形检测_使用OpenCV[Java]检测简单几何图形
  14. Ubuntu解决RTNETLINK answers: File exists
  15. windows自动化操作——程序员必备
  16. 运维之DNS域名解析服务基础概念与Bind9安装
  17. 自动化测试框架[Cypress元素操作详解]
  18. 南科大学生对网络授课的反馈
  19. Unity3D学习:射击小游戏——飞碟世界
  20. 上海交通大学转专业到计算机,2018级自主转专业终审公示

热门文章

  1. 崩坏3android版礼包,崩坏3永久有效兑换码大全 崩坏3永久有效礼包兑换码汇总
  2. DeviceIoControl接口
  3. 客户最佳实践 | All in企业微信,苏州吴中维多利亚美容医院让每位员工成为“美丽管家”!
  4. node.js把前台传来的base64码转成图片存放
  5. Advanced Installer轻松带你入门
  6. 浅谈数据结构-最短路径
  7. 鹤岗房价为啥这么低?哪些城市也可以5万买一套房
  8. 电容充电放电时间计算公式
  9. 通栏导航栏的制作,综合使用CSS属性,代码不超过30行
  10. CSR8675的DSP学习笔记——a2dp_sink工程的matlab仿真