《面试必问-HashMap》通俗易懂搞定HashMap底层原理

不一定逆风翻盘,但一定向阳而生!


HashMap的底层原理是面试必考题,为什么面试官如此青睐这道题?

HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好offer,这是一个迈不过的坎,接下来我用最通俗易懂的语言带着大家揭开HashMap的神秘面纱!

目录

一、HashMap的结构和实现原理

二、HashMap在java1.8和java1.7中有什么区别

三、HashMap的扩容机制

四、put操作如何实现的?

五、HashMap的并发问题

六、一般用什么作为HashMap的key

总结


一、HashMap的结构和实现原理

  • 看过HashMap源码么?能聊聊他的结构么?

那必须看过!HashMap是我们常用的数据结构,由数组和链表组合构成,在java8后,链表长度大于8时,链表会转成红黑树,链表长度小于6时,会由红黑树转为链表。

用一张图简单体现:

 

  • 为什么使用数组+链表的结构?

数组里每个地方都存了key-value这样的实例,在put操作的时候会根据key的hash值去计算index(即在数组中的位置)

ps:这里key的hash值是将hashcode高低十六位异或过的,源码如下

我们都知道数组的长度是有限的,在有限的长度里我们使用哈希,哈希本身就存在概率性,如果两个key的hash值是相同的,即发生hash碰撞,就会形成链表。

  • 解释一下什么是hash碰撞(hash冲突)?

在解释hash碰撞之前首先简单了解hash算法和hash表。

hash算法就是把输入的值通过散列算法计算得出一个散列值。

hash表:又叫做“散列表”,它是通过key直接访问到内存存储位置的数据结构,在具体实现上,我们通过hash函数把key映射到表中某个位置,来获取这个位置的数据,从而加快数据的查找。

在计算hash地址的过程中会出现对于不同的关键字出现相同的哈希地址的情况,即key1 ≠ key2,但是f(key1) = f(key2),这种情况就是Hash 碰撞

  • 如何解决hash碰撞?

1、开放地址法:也称为线性探测法,就是从发生冲突的位置开始,按照一定次序(顺延)从hash表找到一个空闲位置,把发生冲突的元素存到这个位置。ThreadLocal就是用这种方法解决hash碰撞。

2、链地址法:就是把冲突的key,以单向链表来进行存储,比如HashMap

3、再哈希法:就是存在冲突的时候,再hash,一只运算知道不再产生冲突

  • HashMap是如何解决hash碰撞的?

在java8中是通过链地址法以及红黑树解决,其中红黑树是为了优化hash表的链表过长导致时间复杂度增加的问题。

  • 如何控制hash碰撞的概率?

好的hash算法和扩容机制

二、HashMap在java1.8和java1.7中有什么区别

  • 结构不同

java1.7是数组+链表结构,java1.8中是数组+链表+红黑树

  • 插入方式不同

java1.7采用的是头插法,java8采用的是尾插法。

java1.7先扩容在插入数据,java1.8是先插入数据后扩容

扩容时java1.7需要rehash,在java1.8中不需要重新计算hash值。

三、HashMap的扩容机制

  • 何时扩容

1、数组为空时即tab==null 或者tab.length==0

2、元素个数超过数组长度*负载因子的时候,load_factor:负载因子,默认值0.75,DEFAULT_INITIAL_CAPACITY:初始容量,1<<4即16

3、当链表长度大于8且数组长度小于64时

  • 如何扩容

先创建一个新的Entry空数组,长度是原数组的2倍,遍历原Entry数组。

如果oldTab[i]只有一条数据,没有形成链表,那么直接按照公式存放数据

newTab[e.hash & (newCap - 1)]当前节点存放的hash&新数组容量-1,jdk1.8中无需在rehash。

  • 为什么扩容是2的次幂

HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

四、put操作如何实现的?

  • put操作

put操作具体实现可以通过下图来理解,有兴趣可以结合流程图看源码可以更清楚

put操作需要注意的点:

1、仅在添加新的数据的时候才会判断是否需要扩容,如果是覆盖value或者插入红黑树是不需要扩容的。

2、链表转为红黑树需要满足条件:链表长度大于8且数组长度大于等于64,否则是做扩容。

3、判断key是否相同是先hashcode是否相同,再判断equals是否相同。

ps:HashMap需要重写Object的hashCode方法和equals方法

4、数组索引下标的计算方法:

hash^(hash>>>16)&length-1

这么做的原因就是减少hash碰撞,使数据均匀分配。

五、HashMap的并发问题

  • HashMap是线程安全的么?

HashMap是线程不安全的。

  • 为什么说HashMap是线程不安全的?

死循环、数据丢失:java7以前HashMap采用头插法,多线程扩容就会引起链表顺序倒置,形成死循环,数据丢失,java8改成尾插法后,死循环和数据丢失的问题已经解决。

数据覆盖:HashMap在执行put操作时,因为没有加同步锁,多线程put可能会导致数据覆盖

  • 如何解决HashMap线程不安全的问题?

一般我们会使用ConcurrentHashMap或者HashTable

1、HashTable是直接在方法上加锁(即用synchronized关键字修饰方法),简单粗暴,效率低不推荐。

2、使用Collections.synchronizedMap(new HashMap()),这个方法返回一个SynchronizedMap,该内部类中维护了一个普通的map和一个对象排斥锁mutex。如果我们在构造方法中传入了mutex,就使用我们传入的互斥锁,如果没有传入,就是用当前的对象锁。然后在方法上,全部加上synchronized,类似于HashTable

3、ConcurrentHashMap效率相对高一些。所以存在线程不安全的场景我们都使用的是ConcurrentHashMap。由于内容太多具体实现后面我会单独写一篇介绍,在这就不多介绍。

六、一般用什么作为HashMap的key

  • key可以为null么?

可以。

  • 一般用什么作为HashMap的key?

一般用Integer,String这种不可变类作为key,String 最为常用,如果用可变类作为key,它的hashcode可能会发生改变,导致put进去的值无法get出。

总结

HashMap绝对是最常被问的集合之一,因为内容太多,部分知识点我会单独写一篇,核心内容基本都已经体现到。

如果本篇文章有任何错误,请大家多多包涵批评指教,不胜感激!

我是酱子(关注微信公众号:爪哇酱子),感谢大家对本期文章的阅读,创作不易,各位的支持和认可是我最大的动力,如果觉得文章写的不错的话,就请各位点赞在看关注,我们下期见~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值