Hashmap主要流程源码解读
导读
首先,我先提出几个关于Hashmap的问题,如果你全部都会,那么恭喜你,不用往下看了。
-
Hashmap为什么这么快
-
Hashmap的荷载系数为什么是0.75
-
Hashmap的容量为什么是2的幂次方
-
Hashmap解决hash冲突的方式有哪些
-
Hashmap计算数组下标的方式是什么,为什么
-
Hashmap的put流程以及get流程
我将一一回答这些问题,并通过源码进行讲解,相信你一定能有所收获。
解决问题
Hashmap为什么这么快
这个算是比较简单的一个问题,我们只需要通过对Hashmap的get流程进行分析,就可以轻易的得出来。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
为了简洁,我就不将getNode()的代码贴出来了。getNode()的主要执行流程是通过hash(key)方法进行(n - 1) & hash(ps:n是数组长度)运算得到数组下标,如果对应的数组上不为空,会遍历该位置上的链表,使用equals对比k值,这也是为什么getNode()需要传key,最后返回。通过以上操作,避免了对所有值遍历,只需要遍历一小部分,从而加快了获取值的速度。
Hashmap的荷载系数为什么是0.75
这个问题比较简单,算是put流程的一部分,当数组长度大于荷载系数时会扩容(比如数组长度长度是16,在put元素时会判断,如果长度大于16*0.75=12,就会扩容)。为什么是0.75可以从两方面回答,如果小于0.75,会在数组还有很多空余的情况下扩容,浪费空间。如果大于0.75,会容纳更多的元素,增加hash冲突的概率,得不偿失。
Hashmap的容量为什么是2的幂次方
Hashmap的构造方法以及扩容方法中,会执行这样一个方法。
/**
* 返回给定目标容量的 2 大小的幂。
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
通过这个方法,返回大于目标长度的最小2的幂次方,比如传12返回16,传16返回32。为什么Hashmap的容量就一定要是2的幂次方呢?原因就是
Node<K,V>[] tab[i = (n - 1) & hash]
i就是数组下标,put元素会频繁的进行(n - 1) & hash运算,这个运算等价于 hash % (n - 1)。因为位运算效率远远高于取余运算,但是要使(n - 1) & hash等价于 hash % (n - 1),n必须是2的幂次方,否则不行,所以Hashmap的容量是2的幂次方。
Hashmap解决hash冲突的方式有哪些
相信大多数同学都能很轻松的回答出来,Hashmap解决hash冲突的方式主要有两种,在哈希法以及红黑树法。红黑树的条件,链表长度大于8,数组长度大于64。在哈希法发生在扩容时,扩容分为两种
- 当链表长度大于8但是数组长度小于64时
- 当数组容量大于数组长度的0.75(荷载因子)
Hashmap怎么计算哈希值的,为什么
/**
计算 key.hashCode() 并将较高的哈希位传播 (XOR) 到较低的哈希位。由于该表使用二次幂掩码,因此仅在当前掩码上方的位数上变化的哈希集将始终发生冲突。(已知的例子包括一组 Float 键,在小表中保存连续的整数。因此,我们应用了一个转换,将更高位的影响向下分散。在速度、效用和位传播质量之间需要权衡。因为许多常见的哈希集已经合理分布(所以不会从传播中受益),并且因为我们使用树来处理箱中的大量碰撞,所以我们只是以最便宜的方式对一些移位进行 XOR 以减少系统损失,并合并最高位的影响,否则由于表边界,这些位永远不会在索引计算中使用。
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
源码中对应的有注解,一句话总结就是将高16位扰动到底16位,避免因为数据半段不一样但后半段一样造成的hash冲突。
Hashmap的put流程
经过上面的逐步说明,put流程就可以很轻易的总结了
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看出来put的主要流程是putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//初始化节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果map为空或者长度是0,初始化长度
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;
//对应的数组下标有数据
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);
//链表长度大于8,走树逻辑,会先判断数组长度是否大于64,小于64会扩容,大于64会树化
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;
}
总结
以上,就是我对Hashmap的全部理解了,希望能够帮助到你,如果还是很模糊,可以去B站上搜Hashmap源码,找到对应的课程学习。