1. 简述一下Java HashMap的实现原理
HashMap
底层维护了一个Node
类型的数组table
, 默认是Null
,table
数组中的元素是一个链表,并在某些条件下会将链表转化为红黑树。
当创建HashMap
对象时,会将加载因子(loadFactor)初始化为0.75
当我们向table
数组中添加一个K-V
键值对时,会根据当前的Key
得到一个hash
值,然后在底层将这个hash
值转化为一个索引值(index = hash & (cap - 1)
),这个索引值决定了该元素在table
数组中应该存放的位置。
当table
数组索引处对应的位置没有元素存在时,直接将该键值对
加入到table
数组中。反之,则进行判断,如果当前要添加的键值对的key
与该位置的键值对key
相等,则放弃添加该元素,并用新的value
替换掉旧的value
,同时返回旧值。如果当前元素的key
与已存在元素的key
不相等,则判断table
数组改索引处的后面元素是一个链表
还是一颗红黑树
。
若判断table
数组该索引的位置是一个链表,则继续判断该链表中的元素有无与当前元素相同的key
,若有,则用新值替换旧值,同时返回旧值。若无,则直接将当前的键值对
挂在到链表的后面。若判断该索引处的元素是一颗红黑树,则用红黑树的方式去添加元素。
第一次向HashMap
中添加元素时,HashMap
会先进行一次扩容,默认容量是16,同时计算出一个临界阈值:容量 * 0.75 = 12;之后,当table
数组中的元素超过临界值时,就要对table
数组进行扩容,新容量是就旧容量的2倍,同时计算出一个新的临界阈值,以此类。
在JDK17中,如果table
数组中的某一条链表的元素个数达到或者超过了TREEIFY_THRESHOLD (默认是8),并且table数组的长度达到或者超过了MIN_TREEIFY_CAPACITY(默认是64),则底层会将该链表进行树化,将其转换为一颗红黑树,否则仍然采用数组扩容机制。
2.能详细说一下HashMap的扩容过程吗
当HashMap
计算出一个新的容量值后,内部会重新创建一个新的Node数组table
,接着会遍历旧的table
,然后做如下判断:
- 如果索引下只有一个元素,则使用该元素的
hash
值结合新的table
容量计算出一个新的索引,并将该元素放入新table
索引位置,同时将旧table
索引下的元素置空。 - 如果这个索引处是一个链表,则要对链表中的所有元素的
hash
值进行重新的插入。具体做法是:基于元素的hash
值,将他们拆分成两部分,低位部分继续保存在原索引(oldIndex
)位置,高位部分则放入[oldIndex + oldCap]
。这种做法的数学原理,当容量扩大二倍后, 如果一个元素的hash & oldCap == 0
,那么该Hash值使用newCap计算出的新index与旧index相同,而hash & oldCap == 1
的元素,使用newCap计算出的newIndex = (oldIndex + oldCap)
- 如果该索引处是一个树,则遍历该树上的全部元素,并根据
hash
拆分成两部分,低位部分和高位部分,低位部分放入旧索引位置,高位部分放入[oldIndex + oldCap]
索引位置。如果两部分的节点个数小于UNTREEIFY_THRESHOLD
(通常为6
),则将其转换成一个链表结构,否则将其转换为树结构。