【Java核心面试宝典】Day3、图解HashMap高频面试及底层实现架构!

**ConcurrentHashMap:**这是现阶段使用使用比较多的一种线程安全的Map实现类。在1.7以前使用的是分段锁机制实现的线程安全的。但是在1.8以后使用synchronized关键字实现的线程安全。

其中关于HashMap的考察和提问在面试中是最频繁的,这也是在日常开发中最应该深入理解和掌握的。所以接下来就主要和大家详细分析一下HashMap的实现原理以及面试中的常考问题。

3、请阐述HashMap的put过程?

=======================

我们知道HaahMap使用put的方式进行数据的存储,其中有两个参数,分别是key和value,那么关于这个键值对是如何进行储存的呢?我们接下来进行分析一下。

在HashMap中使用的是数组+链表的实现方式,在HashMap的上层使用数组的形式对“相同”的key进行存储,下层对相应的key和value使用链表的形式进行链接和存储。

**注意:这里所说的相同并不一定是key的数值相同,而是存在某种相同的特征,**具体是哪种特征骂我们继续往下看!

HashMap将将要存储的值按照key计算其对应的数组下标,如果对应的数组下标的位置上是没有元素的,那么就将存储的元素存放上去,但是如果该位置上已经存在元素了,那么这就需要用到我们上面所说的链表存储了,将数据按照链表的存储顺序依次向下存储就可以了。这就是put的简单过程,存储结果如下:

但是我们有时候存储的数据会很多,那么如果一直使用链表的形式进行数据的存储的话就或造成我们的链表的长度非常大,这样无论在进行删除还是在进行插入操作都是十分麻烦的,因此对于这种情况应该怎么办呢?

**这里就涉及到了一个链表中数据存储时,进行“树化”和“链化”的一个过程,**那么什么是“树化”和“链化”呢?

当我们在对键值对进行存储的时候,如果我们在同一个数组下标下存储的数据过多的话,就会造成我们的链表长度过长,导致进行删除和插入操作比较麻烦,所以在java中规定,**当链表长度大于8时,我们会对链表进行“树化”操作,****将其转换成一颗红黑树(一种二叉树,左边节点的值小于根节点,右边节点的值大于根节点),**这样我们在对元素进行查找时,就类似于进行二分查找了,这样的查找效率就会大大增加。

但是当我们进行删除操作,将其中的某些节点删除了之后,链表的长度不再大于8了,这个时候怎么办?难道就要赶紧将红黑树转化为链表的形式吗?其实并不是,只有当链表的长度小于6的时候,我们才会将红黑树重新转化为链表,这个过程就叫做“链化”。

过程图示如下:

那么为什么要在长度8的时候进行“树化”,而在长度小于6的时候才进行“链化”呢?为什么不直接在长度小于8的时候就进行“链化”?

**主要原因是因为:**当删除一个元素,链表长度小于8的时候直接进行“链化”,而再增加一个元素,长度又等于8的时候,又要进行“树化”,这样反复的进行“链化”和“树化”操作特别的消耗时间,而且也比较麻烦。所以程序就规定,只有当当链表长度大于等于8的时候才进行“树化”,而长度小于6的时候才进行“链化”,其中关于8树化、6链化这两个阈值希望大家牢记!

4、链表中是按照怎样的顺序存放数据的?

=======================

我们现在已经知道了HashMap中的元素是如何存放的,但是有时候面试官可能还会问我们,在HashMap中,向链表中存储元素是在头结点存储的还是在尾节点存储的?

这个我们需要知道,对于HashMap中链表元素的存储。

在JDK1.7以及前是在头结点插入的,在JDK1.8之后是在尾节点插入的。

5、Hash(key)方法是如何实现的?

========================

我们现在已经知道了HashMap中的元素是如何存储的了,那么现在就是如何应该根据key值进行相应的数组下标的计算呢?

我们知道HashMap的初始容量是16位,那么对于初始的16个数据位,如果将数据按照key的值进行计算存储,一般最简单的方法就是根据key值获取到一个int值,方法是:

int hashCode = key.hashCode()

然后对获取到的hashCode与16进行取余运算,

hashCode % 16 = 0~15

这样得到的永远都是0—15的下标。这也是最最原始的计算hash(key)的方法。

**但是在实际情况下,这种方法计算的hash(key)并不是最优,**存放到数组中的元素并不是最分散的,而且在计算机中进行余运算其实是非常不方便的、

**所以为了计算结果尽可能的离散,现在计算数组下标最常用的方法是:**先根据key的值计算到一个hashCode,将hashCode的高16位二进制和低16位二进制进行异或运算,得到的结果再与当前数组长度减一进行与运算。最终得到一个数组下标,过程如下:

int hashCode = key.hashCode()

int hash = hash(key) = key.hashCode()的高16位^低16位&(n-1)  其中n是当前数组长度

同时在这里要提醒一点。

在JDK1.7和JDK1.8的时候对hash(key)的计算是略有不同的

JDK1.8时,计算hash(key)进行了两次扰动

JDK1.7时,计算hash(key)进行了九次扰动,分别是四次位运算和五次异或运算

其中扰动可能理解为运算次数

以上就是Hash(key)方法的实现过程。

6、为什么HashMap的容量一直是2的倍数?

===========================

HashMap的容量之所以一直是2的倍数,其实是与上面所说的hash(key)算法有关的,

原因是只有参与hash(key)的算法的(n-1)的值尽可能都是1的时候,得到的值才是离散的。假如我们当前的数组长度是16,二进制表示是10000,n-1之后是01111,使得n-1的值尽可能都是1,对于其他是2的倍数的值减1之后得到值也是这样的。

所以只有当数组的容量长度是2的倍数的时候,计算得到的hash(key)的值才有可能是相对离散的,

7、Hash冲突如何解决?

=================

什么是Hash冲突?就是当我计算到某一个数组下标的时候,该下标上已经存放元素了,这就叫Hash冲突,很显然,如果我们计算数组下标的算法不够优秀的时候,很容易将存储的数据积累到同一个下标上面,造成过多的Hash冲突。

那么如何解决hash冲突?

最应该解决的其实就是让存储的key计算得到的数组下标尽可能的离散,也就是要求hash(key)尽可能的优化,数组长度是2的倍数。这也就是Hash冲突的主要解决方法。

具体可以查看下面HashMap关键部分的底层源码:

Hash(key)的底层实现

/**

  • Applies a supplemental hash function to a given hashCode, which

  • defends against poor quality hash functions. This is critical

  • because HashMap uses power-of-two length hash tables, that

  • otherwise encounter collisions for hashCodes that do not differ

  • in lower bits. Note: Null keys always map to hash 0, thus index 0.

*/

static int hash(int h) {

// This function ensures that hashCodes that differ only by

// constant multiples at each bit position have a bounded

// number of collisions (approximately 8 at default load factor).

h ^= (h >>> 20) ^ (h >>> 12);

return h ^ (h >>> 7) ^ (h >>> 4);

}

put(key,value)方法的底层实现

/**

  • Associates the specified value with the specified key in this map.

  • If the map previously contained a mapping for the key, the old

  • value is replaced.

  • @param key key with which the specified value is to be associated

  • @param value value to be associated with the specified key

  • @return the previous value associated with key, or

  •     <tt>null</tt> if there was no mapping for <tt>key</tt>.
    
  •     (A <tt>null</tt> return can also indicate that the map
    
  •     previously associated <tt>null</tt> with <tt>key</tt>.)
    

*/

public V put(K key, V value) {

if (key == null)

return putForNullKey(value);

int hash = hash(key.hashCode());

int i = indexFor(hash, table.length);

for (Entry<K,V> e = table[i]; e != null; e = e.next) {

Object k;

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

V oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

}

}

modCount++;

addEntry(hash, key, value, i);

return null;

}

8、HashMap是如何扩容的?

====================

我们在上面说到了HashMap的数组的初始容量是16,但是很显然16个存储位是显然不够的,那么HashMap应该如何扩容呢?

在这里需要用到一个参数叫“扩容因子”,在HashMap中“扩容因子”的大小是0.75,

**我们上面也提到过,对于初始长度为16的数组,当其中存储的数据长度等于16*0.75=12时。就会对数组元素进行扩容,扩容量是原来数组容量的2倍,**也就是当前是15话,再扩容就是扩容32个数据位。

9、扩容后元素怎么存放的?

最后

这份文档从构建一个键值数据库的关键架构入手,不仅带你建立起全局观,还帮你迅速抓住核心主线。除此之外,还会具体讲解数据结构、线程模型、网络框架、持久化、主从同步和切片集群等,帮你搞懂底层原理。相信这对于所有层次的Redis使用者都是一份非常完美的教程了。

image

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!!!

12时。就会对数组元素进行扩容,扩容量是原来数组容量的2倍,**也就是当前是15话,再扩容就是扩容32个数据位。

9、扩容后元素怎么存放的?

最后

这份文档从构建一个键值数据库的关键架构入手,不仅带你建立起全局观,还帮你迅速抓住核心主线。除此之外,还会具体讲解数据结构、线程模型、网络框架、持久化、主从同步和切片集群等,帮你搞懂底层原理。相信这对于所有层次的Redis使用者都是一份非常完美的教程了。

[外链图片转存中…(img-Bl8vMoOM-1714510454412)]

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!!!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值