HashMap源码解析(负载因子,树化策略,内部hash实现 resize策略)
讲在前面的小知识点:
哈希表(k,v):数组
根据相应的哈希算法计算key,返回值即为value存储的数组下标
哈希算法:f(k)->int即为v需要存储的数组下标
哈希冲突解决办法:
哈希算法计算的两个不同对象的哈希值相等的情况
eg:1%16 == 17%16
1、开放定址法:
寻找下一个为空的数组下标,而后将冲突元素存储
2、再散列法
再次使用一个不同的哈希算法再次计算一次
3、链地址法:HashMap使用此方法解决哈希冲突
将所有冲突元素按照链表存储
哈希表(k,v)
HashMap维护一个数组,每个数组中根据key值放入链表,链表达到一定长度后转化为红黑树。
== 内部属性:==
1> 负载因子:final float loadFactor(默认为0.75f)
2> 实际容量:int threshold = loadFactor*tab.length;
3> 树化阈值:int TREEIFY_THRESHOLD = 8;
4> 解除树化阈值: int UNTREEIY_THRESHOLD = 6;
这里通过源码就能很好的看出HashMap中的一些基本实现规则
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第一次put时,将哈希表初始化
//resize()1.完成哈希表初始化2.完成哈希表扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//当目标索引未存储元素时,将当前元素存储到目标范围之内
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//哈希表已经初始化并且算出的哈希表位置已经有元素
Node<K,V> e; K k;
//若索引下标对应的元素key恰好与当前元素key值相等且不为null
//将value替换为当前元素的value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//此时索引对应的链表已经树化,采用红黑树方式将当前节点添加到树中
else if (p instanceof TreeNode)
//以链表方式将当前节点添加到链表末尾
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
//找到链表末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//扩容策略 此时添加了新节点
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
从源码可以得出以下几点:
1.HashMap也采用懒加载策略,第一次put时初始化哈希表;
2.树化逻辑:索引下标对应的链表长度达到阈值8并且当前哈希表长度达到64才会树化,否则只是调用resize方法进行哈希表扩容。
3.resize():扩容为原先数组的2倍
4.负载因子过大或导致哈希冲突明显增加,节省内存
负载因子过小会导致哈希表频繁扩容,内存利用率低
5.当链表长度过长时,会将哈希表查找的时间复杂度退化为o(n),树化保证即便在哈希冲突严重时,查找的时间复杂度也为o(logn)
6.当红黑树节点个数在扩容或删除元素时减少为6以下,在下次resize过程中会将红黑树退化为链表,节省空间
7.为何不直接使用哪个Object提供的hashCode
(h = key.hashCode())^(>>>16)
将哈希码保留一半,将高低位都参与哈希算法,减少内存开销,减少哈希冲突。
8.resize():1.完成哈希表初始化2.完成哈希表扩容
9.put内部逻辑:
1>哈希表索引下标计算:
i = (n -1 )&hash
问什么要做与运算:保证求出的索引下标都在哈希表的长度范围之内
2>.n:哈希表长度
n必须为2^n:保证Hash表中的所有索引下标都会被访问到
若n等于15,则以下位置元素永不可能存储元素
0011
0101
1001
1011
1111
15:0000 1111