前言
又到了一年一度的金三银四面试季,我们拿着自己的面试秘籍去面试,但是面试官的问题五花八门,让我们摸不清他们的套路。今天我就总结了面试时必问的hashmap面试题,无论面试官怎么问,我们都对答如流。
另外本人整理了20年至23年的面试题大全,包含spring、并发、数据库、Redis、分布式、dubbo、JVM、微服务等方面总结,下图是部分截图,需要的话点这里点这里,暗号CSDN。
1.JDK8中的HashMap与JDK7的HashMap有什么不一样?
- JDK8中新增了红黑树,JDK8是通过数组+链表+红黑树来实现的
- JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
- JDK8中的因为使用了红黑树保证了插入和查询了效率,所以实际上JDK8中
的Hash算法实现的复杂度降低了 - JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否查过了
阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值。 - JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容
2.HashMap中PUT方法的流程?
- 通过key计算出一个hashcode
- 通过hashcode与"与操作"计算出一个数组下标
- 在把put进来的key,value封装为一个entry对象
- 判断数组下标对应的位置,是不是空,如果是空则把entry直接存在该数组位
置 - 如果该下标对应的位置不为空,则需要把entry插入到链表中
- 并且还需要判断该链表中是否存在相同的key,如果存在,则更新value
- 如果是JDK7,则使用头插法
- 如果是JDK8,则会遍历链表,并且在遍历链表的过程中,统计当前链表的元
素个数,如果超过8个,则先把链表转变为红黑树,并且把元素插入到红黑树中
3.JDK8中链表转变为红黑树的条件?
- 链表中的元素的个数为8个或超过8个
- 同时,还要满足当前数组的长度大于或等于64才会把链表转变为红黑树。为什么?因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解决链表过长的问题。
4.HashMap扩容流程是怎样的?
- HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,
所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新
数组上来,这样才是数组的扩容 - 在HashMap中也是一样,先新建一个2被数组大小的数组
- 然后遍历老数组上的没一个位置,如果这个位置上是一个链表,就把这个链
表上的元素转移到新数组上去 - 在这个过程中就需要遍历链表,当然jdk7,和jdk8在这个实现时是有不一样
的,jdk7就是简单的遍历链表上的没一个元素,然后按每个元素的hashcode结
合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和
之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个
链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率 - 而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到
一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素
时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍
历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新
位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,
一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用
拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的
位置,否则把单向链表放到对应的位置。 - 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组
会被回收到。
5.为什么HashMap的数组的大小是2的幂次方数?
JDK7的HashMap是数组+链表实现的
JDK8的HashMap是数组+链表+红黑树实现的
当某个key-value对需要存储到数组中时,需要先生成一个数组下标index,并且这个
index不能越界。
在HashMap中,先得到key的hashcode,hashcode是一个数字,然后通过
hashcode & (table.length - 1) 运算得到一个数组下标index,是通过与运算计算出
来一个数组下标的,而不是通过取余,与运算相比于取余运算速度更快,但是也有一
个前提条件,就是数组的长度得是一个2的幂次方数。
6、常见的 HashMap 的迭代方式
在实际开发过程中,我们对于 HashMap 的迭代遍历也是常见的操作,HashMap 的迭代遍历常用方式有如下几种:
-
方式一:迭代器模式
Map<String, String> map = new HashMap<>(16);
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> next = iterator.next();
System.out.println(next.getKey() + ":" + next.getValue());
} -
方式二:遍历 Set>方式
Map<String, String> map = new HashMap<>(16);
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
} -
方式三:forEach 方式(JDK8 特性,lambda)
Map<String, String> map = new HashMap<>(16);
map.forEach((key, value) -> System.out.println(key + ":" + value)); -
方式四:keySet 方式
Map<String, String> map = new HashMap<>(16);
Iterator<String> keyIterator = map.keySet().iterator();
while (keyIterator.hasNext()) {
String key = keyIterator.next();
System.out.println(key + ":" + map.get(key));
}
把这四种方式进行比较,前三种其实属于同一种,都是迭代器遍历方式,如果要同时使用到 key 和 value,推荐使用前三种方式,如果仅仅使用到 key,那么推荐使用第四种。
7.为什么说HashMap是线程不安全的?
答:HashMap在多线程并发时线程不安全,主要表现在下面两个方面:
(1) 当向HashMap中put(添加)元素时导致的多线程数据不一致
比如有两个线程 A 和 B ,首先 A 希望插入一个 key-value键值对到HashMap 中,它首先计算记录所要落到的 hash 桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程 A 的时间片用完了,而此时线程 B 被调度得以执行,和线程 A 一样执行,只不过线程 B 成功将记录插到了桶里面。假设线程 A 插入的记录计算出来的 hash 桶索引和线程 B 要插入的记录计算出来的 hash 桶索引是一样的,那么当线程 B 成功插入之后,线程 A 再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程 B 插入的记录,这样线程 B 插入的记录就凭空消失了,造成了数据不一致的行为。
简单来说就是在多线程环境下,向HashMap集合中添加元素会存在覆盖的现象,导致了线程不安全。
(2) 当HashMap进行扩容调用resize()函数时引起死循环
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
HashMap的线程不安全主要体现在下面两个方面:
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
8.HashMap 的工作原理是什么?
一,存储方式: Java中的HashMap是以键值对(key-value)的形式存储元素的。
二,调用原理: HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。
HashMap的实现原理:
-
利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
-
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
-
获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值。
-
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
最后:
针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。
下面的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2020至2023年收集的一些大厂的面试真题(都整理成文档,小部分截图),有需要的可以点击进入暗号CSDN