HashMap底层原理及其扩容机制

HashMap的底层原理与扩容机制一直是面试经常考察的内容,这个知识点粗看很简单,但是细节较多,特此整理。

首先罗列出查阅过程中参考的一些博客,以便日后查询,讲的都很好。

①《经典的HashMap面试点及底层原理》https://blog.csdn.net/lqiaomu/article/details/103672953

②《一文讲清楚HashMap底层构成以及扩容原理》https://zhuanlan.zhihu.com/p/113332649

③《Java HashMap计算初始数组大小过程》(源码分析) https://blog.csdn.net/u012894808/article/details/107023179

④《HashMap的扩容机制》https://zhuanlan.zhihu.com/p/114363420

⑤《hashmap的默认扩容阈值是大于12还是大于等于12https://www.cnblogs.com/wudb/p/12980680.html

⑥《HashMap的负载因子初始值为什么是0.75?》 https://blog.csdn.net/weixin_49448233/article/details/108984279

⑦《我说我了解集合类,面试官竟然问我为什么HashMap的负载因子不设置成1!?》https://blog.csdn.net/weixin_44259720/article/details/104497379

⑧《图解HashMap扩容和ArrayList的扩容机制》https://blog.csdn.net/weixin_43629719/article/details/100024493

HashMap原理

HashMap是java提供一种数据结构,表内由键值对构成,key-value,且key不可重复。在底层实现中HashMap利用数组加链表的结构(Java8中当链表长度过长时候将链表变为红黑树结构,从而提高查询速度)。在想表中添加键值对的过程中,HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过(n-1)&hash判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的key值 是否相同(用equals来比较),如果相同的话,直接覆盖,不相同就通过拉链法(也就是在改链表后面增加节点)解决冲突。 所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode()方法(也就是得到的hashCode分散的不够均匀,导致碰撞较多)换句话说使用扰动函数之后可以减少碰撞。

接下来以问答形式回答一些细节问题

1.扩容机制

Q1:能介绍一下扩容机制么?

A1:若new一个HashMap时没有指定容量(数组大小),则HashMap初始为null,即没有大小,在第一次执行put()时会将容量初始化为16,默认加载因子为0.75,扩容阈值                        Threadhold则为容量capacity*load factor(16*0.75=12),当Map中元素个数超过阈值后,进行扩容机制,容量变为原来的两倍(容量必须为2的幂次,且每次扩容都为2倍)。            若new一个HashMap时候若指定了容量,比如 HashMap<Integer,Integer> map = new HashMap<>(33); 指定了数组大小为33,那数组大小就是33了吗? 显然不是,这样new了            以后,会根据指定的正整数“33”找到不小于指定容量的2的幂数)(源码内部是以位运算实现的,速度快),将这个数设置赋值给阈值(threshold)。第一次调用put方法时,会            将阈值赋值给容量,然后让阈值=0.75*容量。

Q2:为什么每次都是2倍扩容,为什么容量必须是2的幂次?

A2:因为各个key的hash值取值情况非常多,远远大于HashMap容量,原理上需要用hash%capacity取余操作来进行碰撞检测,但对于2的幂次数n,可以使用hash&(n-1)来完成取余            操作,且位运算速度快,所以选择2的幂次作为容量大小。

Q3:是大于等于阈值还是大于阈值时候执行扩容呢?

A3:是大于阈值(12)才进行扩容,也就是第十三次put() 才扩容。

Q4:大于阈值则执行扩容,指的是HashMap中元素个数大于阈值,还是占用的数组位置数量大于阈值呢?

A4:是指HashMap中总元素个数大于阈值就会执行扩容。举个极端例子,想象一下这个情况:假设有12个元素都落到了数组的同一个位置(当然现实情况这种机率非常非常小,几乎没有),数组只占用了一个位置, 那么为什么要扩容呢,还有那么多位置没用呢? 其实这里之所以要扩容,是有一个隐含的逻辑,如果元素总个数大于阈值,而数组占用位置没达到阈值,说明这些元素在当前长度下,分布是“不均匀”的,扩容是为了让其分布“更均匀”

Q5:为什么扩容因子是0.75?

A5:时间与空间的tradeoff 理论上我们认为负载因子不能太大,不然会导致大量的哈希冲突,也不能太小,那样会浪费空间。一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。

Q6:为什么要扩容?

A6:为了避免HashMap发生大量的哈希冲突,所以需要在适当的时候对其进行扩容。对于每一个输入,使用哈希函数可以计算得到相应的输出,不同输出对应的输入肯定不同,而不同输入对应的输出可能是一样的(哈希冲突)。HashMap中产生哈希碰撞的原因有两个①hash函数性能差,不能均匀分散②数组太小,容易碰撞;

Q7:为什么初始长度设置为16?

A7:这个问题很玄学,但是根据hashmsp的源码注释来看,作者指明了选取16是根据泊松分布取的;(之后提到的冲突策略中链表、红黑树分界值7页是根据泊松分布选取)

Q8:自动扩容(resize)具体步骤

A8:https://blog.csdn.net/weixin_39951929/article/details/113076194?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242

        首先会创建一个容量x2的新数组,然后将原来的Entry复制到新数组中去,下标和原来一样或者是原来的下标加上原容量(hash&(2*length-1) )此过程为rehash。 再将Entry复制          到新数组中去的时候。使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(如果发生了hash冲            突的话)。在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。 https://blog.csdn.net/pange1991/article/details/82347284                下图为java1.7,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置JDK1.8不会倒置。先略过红黑树的情况,描述下简单流程,在JDK1.8中发生            hashmap扩容时,遍历hashmap每个bucket里的链表,每个链表可能会被拆分成两个链表,不需要移动的元素置入loHead为首的链表,需要移动的元素置入hiHead为首的链表,          然后分别分配给老的buket和新的buket。

         

Q9:ArrayList扩容机制如何?

A9初始化:ArrayList的底层是一个动态数组,ArrayList首先会对传进来的初始化参数initalCapacity进行判断
                      如果参数等于0,则将数组初始化为一个空数组,
                      如果不等于0,将数组初始化为一个容量为10的数组。
        扩容时机:当数组的大小大于初始容量的时候(比如初始为10,当添加第11个元素的时候),就会进行扩容,新的容量为旧的容量的1.5倍。
        扩容方式:扩容的时候,会以新的容量建一个原数组的拷贝,修改原数组,指向这个新数组,原数组被抛弃,会被GC回收。

2.哈希冲突

当两个key计算得到的hash值相同时,会定位到数组中同一个bucket,此时即产生哈希冲突。首先用equals()判断当前key与该链表中的元素是否有相同的,若相同则value直接覆盖,若不同则在末尾添加新的键值对。Java1.8中引入两种碰撞机制,一个利用链表解决碰撞问题。当链表长度过长时则利用红黑树,目的是提高搜索效率。

Q1:什么时候从链表转为红黑树?

A1:选择‘7’Wie分界值,当链表内节点大于8时从链表转为红黑树,当红黑树内节点小于6时从红黑树转为链表。中间值作为过渡值,防止链表和红黑树互相转换过于频繁,徒增开销。

Q2:为什么选择‘7’作为分界值呢?

A2:因为泊松分布,在负载因子0.75(HashMap默认)的情况下,一个bucket中键值对数量大于7的概率为百万分之一,所以选7做为分界点。(和“为什么选16作为容量初始值”一致,都是因为泊松分布)。

Q3:为什么链表长了以后要改用红黑树?

A3:红黑树插入为O(lgn),查询为O(lgn),链表插入为O(1),查询为O(n)。个数少时,插入删除成本高,用链表;个数多时,查询成本高,用红黑树。

3.HashMap线程安全问题

Q1:HashMap为什么线程不安全(链表成环)?

A1:https://my.oschina.net/hosee/blog/673521

Q2:ConcurrentHashMap为什么是线程安全的?

A2:

 

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值