大厂必问17个HashMap题

1、介绍下 HashMap 的底层数据结构吧

我们现在用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,如下图,而在 JDK 1.8 之前是由“数组+链表”组成。

2、为什么要改成“数组+链表+红黑树”?

主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。

3、那在什么时候用链表?什么时候用红黑树?

a、对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
b、对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。

4、为什么链表转红黑树的阈值是8,回转链表阈值为6?

a、按照泊松分布的公式计算,链表中节点个数为8时的概率非常低,到8个节点时,红黑树的性能优势也会开始展现。
b、6是防止频繁在红黑树和链表之间的转换

5、HashMap 有哪些重要属性?分别用于做什么的?

除了用来存储我们的节点 table 数组外,HashMap 还有以下几个重要属性:
1)size:HashMap 已经存储的节点个数;
2)threshold:扩容阈值,当 HashMap 的个数达到该值,触发扩容。
3)loadFactor:负载因子,扩容阈值 = 容量 * 负载因子。

6、threshold 除了用于存放扩容阈值还有其他作用吗?

在我们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费。

7、HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

8、 HashMap 的容量必须是 2 的 N 次方,这是为什么?

计算索引位置的公式为:(n - 1) & hash,当 n 为 2 的 N 次方时,n - 1 为低位全是 1 的值,此时任何值跟 n - 1 进行 & 运算会等于其本身,达到了和取模同样的效果,实现了均匀分布。实际上,这个设计就是基于公式:x mod 2^n = x & (2^n - 1),因为 & 运算比 mod 具有更高的效率。

9、为什么负载因子loadFactor是0.75而不是其他的

例如1,此时会减少空间开销,但是 hash 冲突的概率会增大,增加查找成本;而如果值较低;
例如 0.5 ,此时 hash 冲突会降低,但是有一半的空间会被浪费,所以折中考虑 0.75 似乎是一个合理的值

10、HashMap 的插入流程是怎么样的

  1. 首次调用put,先给数组扩容,默认为16。
  2. 如果计算出的索引位置没有数据,直接插入元素
  3. 如果有hash冲突,如果key相同,替换value
  4. 如果是树结构,插入树
  5. 如果是链表 :
    a、从尾部插入数据,链表大于8,转为红黑树。
    b、如果key相同,替换value

11、计算 key 的 hash 值,是怎么设计的

拿到key的hashCode值与hashCode的高16位做异或^操作得到重计算的hash值

static final int hash(Object key) {
 int h;
 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

12、为什么要将 hashCode 的高16位参与运算?

如果高位变化明显,低位不明显,如果我们将高位参与运算,则索引计算结果就不会仅取决于低位,减少hash冲突

13、扩容(resize)流程介绍下

 1. 如果桶中只有一个元素,直接插入
 2. 如果是树结构,用树插入。
 3. 如果链表结构,且节点大于1,hash和新数组长度做&运算
	 a、结果为0,元素在新数组的原来的位置
	 b、结果为1,元素在新数组的原来的位置 + 旧数组的长度

14、红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置,这是为什么?

扩容后,新表的 n - 1 只比老表的 n - 1 在高位多了一个1,因此计算新表的索引位置时,只取决于新表在高位多出来的这一位
因为只取决于这一位,所以只会存在两种情况:
1) (e.hash & oldCap) == 0 ,则新表索引位置为“原索引位置” ;
2)(e.hash & oldCap) != 0,则新表索引位置为“原索引 + oldCap 位置”。

15、HashMap 是线程安全的吗?

不是。HashMap 在并发下存在数据覆盖、遍历的同时进行修改会抛出 ConcurrentModificationException 异常等问题,JDK 1.8 之前还存在死循环问题。

16、介绍一下死循环问题

导致死循环的根本原因是 JDK 1.7 扩容采用的是“头插法”,并发的时候可能会导致同一索引位置的节点在扩容后顺序反掉。
a线程执行到 Entry<K,V> next = e.next ,
b线程正常执行完扩容,a线程再执行后续操作会导致死循环的出现
而 JDK 1.8 之后采用的是“尾插法”,扩容后节点顺序不会反掉,不存在死循环问题。

17、总结下 JDK 1.8 主要进行了哪些优化

1、底层数据结构从“数组+链表”改成“数组+链表+红黑树”,主要是优化了 hash 冲突较严重时,链表过长的查找性能:O(n) -> O(logn)。
2、计算 table 初始容量的方式发生了改变,老的方式是从1开始不断向左进行移位运算,直到找到大于等于入参容量的值;新的方式则是通过“5个移位+或等于运算”来计算。
3、优化了 hash 值的计算方式,新的只是简单的让高16位参与了运算
4、扩容时插入方式从“头插法”改成“尾插法”,避免了并发下的死循环。
5、扩容时计算节点在新表的索引位置方式从“h & (length-1)”改成“hash & oldCap”,性能可能提升不大,但设计更巧妙、更优雅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值