(转载) HashMap底层原理 + 扩容机制

目录

一、扩容机制

HashMap看这篇就够了~_技术交流_牛客网

二、HashMap底层原理

一:HashMap的节点

二:HashMap的数据结构

三:HashMap存储元素的过程

四: HashMap具体的存取过程

put存值的方法,过程如下:

get取值的方法,过程如下:

五、HashMap的负载因子为啥是0.75?

侧面回答:

正面回答:

六、为什么 数组容量 是 2 的整数倍?

解决 hash 冲突的常见方法


一、扩容机制

HashMap看这篇就够了~_技术交流_牛客网

初始容量

增长后

增长触发条件

arrayList

10

旧容量*1.5+1

n>10

vector

10

旧容量*2

n>10

hashSet

16=2^4

旧容量*2

n>旧容量*0.75

hashMap

16=2^4

旧容量*2

n>旧容量*0.75

“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”
HashMap的扩容阈值(threshold = capacity* loadFactor );

threshold = capacity * loadFactor当 Size>=threshold的时候,那么就要考虑对数组的扩增了;

就是通过它和size进行比较来判断是否需要扩容。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,将会创建原来HashMap大小的两倍的bucket数组(jdk1.6,但不超过最大容量),来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

JDK1.8 之前:  HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

JDK1.8 以后:  HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n-1) & HashCode 判断当前元素存放的位置(这里的 n 指的是数组的长度);所谓扰动函数指的就是 HashMap 的 hash 方法, 换句话说使用扰动函数之后可以减少碰撞

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次

进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。

二、HashMap底层原理

hashMap为什么线程不安全?[计算hashCode] + [put]  不具有原子性,会产生数据覆盖。另外[put]+[计算size] 也不具有原子性。

arrayList 线程不安全的原因?  [add]  + [计算size] 俩操作不具有原子性。

ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全

一:HashMap的节点

HashMap是一个集合,键值对的集合,源码中每个节点用Node表示,Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用

二:HashMap的数据结构

HashMap的数据结构为 数组+(链表或红黑树),入下图:

为什么采用这种结构来存储元素呢?

数组的特点:查询效率高,插入,删除效率低

链表的特点:查询效率低,插入删除效率高

在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。

三:HashMap存储元素的过程

有这样一段代码:

HashMap<String,String> map = new HashMap<String,String>();
map.put("刘德华","张惠妹");
map.put("张学友","大S");

现在我要把键值对 “刘德华”,”张惠妹”存入map:

第一步:计算出键“刘德华”的hashcode,该值用来定位要将这个元素存放到数组中的什么位置;

              刘德华的hashcode为20977295 数组长度为 16则要存储在数组索引为 20977295%16=1的地方;

第二步:数组索引为1的地方是空的,这种情况很简单,直接将元素放进去就好了。

              若已经有元素占据了索引为1的位置,这种情况下我们需要判断一下该位置的元素和当前元素是否相等,使用equals来比较。

              如果两者相等则直接覆盖如果不等则在原元素下面使用链表的结构存储该元素;

链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高.

preview

四: HashMap具体的存取过程

之前定位桶用的是【取模运算】,后来变成了【位与运算】;据说是位与运算的代价远高于位与运算;

put存值的方法,过程如下:

eg:    map.put("张三+语文",91);

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

【链表---->红黑树】的2个条件:(链表长度大于8) and (数组长度到64.)

【红黑树---->链表】的条件: 红黑树节点数小于等于6个就链表化;

get取值的方法,过程如下:

eg:    map.get("张三+语文");

①.指定key 通过hash函数得到key的hash值
     int hash=key.hashCode();

②.调用内部方法 getNode(),得到桶号(一般为hash值对桶数求模)
     int index =hash%Entry[].length;  取余
     jdk1.6版本后使用位运算替代模运算,int index=hash&( Entry[].length - 1);

③.比较桶的内部元素是否与key相等,若都不相等,则没有找到。相等,则取出相等记录的value。

④.如果得到 key 所在的桶的头结点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。

    getTreeNode 方法使通过调用树形节点的 find()方法进行查找。   

    由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高。

⑤.如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回;不相等就从子树中递归查找。

五、HashMap的负载因子为啥是0.75?

侧面回答:

正面不好回答你就侧面说:

若load Factor = 0.25,那就意味着array的4格被全部填满就会达到扩容阈值扩容到32,这就导致28个没利用很浪费空间;

若load Factor = 1,那就意味着array意味着本该在0.75时扩容没扩,新添加的元素只能挤到链表里,会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利;

总结:负载因子太小,虽然时间效率提升了,但是空间利用率降低了;

           负载因子过大,虽然空间利用率上去了,但是时间效率降低了;

故而,经过验证,0.75最合适;

正面回答:

在HashMap注释中有这么一段:

在理想情况下,使用随机哈希吗,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素的个数概率对照表
从上表可以看出当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为负载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
hash容器指定初始容量尽量为2的幂次方
HashMap负载因子为0.75是空间和时间成本的一种折中

Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006
* more: less than 1 in ten million
*

六、为什么 数组容量 是 2 的整数倍?

key.hashcode % array.length = key.hashcode & (array.length - 1) 等式成立的前提是 array.length 是2的整数倍!即 array.ength长度是2的次幂时!

解决 hash 冲突的常见方法

针对哈希表直接定址可能存在hash冲突,举一个简单的例子,例如:

第一个键值对A进来,通过计算其 key 的 hash 得到的 index=0 。记做:   Entry[0]  =  A 。
第二个键值对B,       通过计算其 index 也等于0, HashMap 会将  B.next  = A,  Entry[0]  = B,
第三个键值对C,       通过计算其 index 也等于0,那么  C.next = B,  Entry[0] = C;


这样我们发现  index=0  的地方事实上存取了  A,B,C  三个键值对,  它们通过next这个属性链接在一起。 对于不同的元素,可能计算出了相同的函数值,这样就产生了hash 冲突,那要解决冲突,又有哪些方法呢?具体如下:

a. 链地址法:             将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

b. 开放定址法:         即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。

c. 再哈希法:             即发生冲突时,由其他的函数再计算一次哈希值。

d. 建立公共溢出区:  将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。

HashMap采用哪种方法解决冲突的呢?

HashMap 就是使用  链地址法  来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。当两个对象的 hashcode 相同时,它们的 bucket 位置相同,碰撞就会发生。此时,可以将 put 进来的 K- V 对象插入到链表的 尾部 。对于储存在同一个bucket位置的链表对象,可通过键对象的equals()方法用来找到键值对。
 

转载文章链接:

文章链接:最通俗易懂搞定HashMap的底层原理 - 知乎

文章链接:https://blog.csdn.net/visant/article/details/80045154

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值