源码讲解网上太多了,能够参考HashMap此生来世。 我这里主要讲解Java 8的HashMap扩容原理。下文中声明的桶与数组的含义是一致的html
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
i = (tab.length - 1) & hash
复制代码
这与咱们平时学的hash表算法不太同样,先说效果:位运算计算快,效率高,与数组长度取模(好比Hashtabal的put方法就是哈希取模)效果同样。 由于桶的个数是2的n次幂,以默认16举例。tab.length -1 =15,二进制就是 1111,与hash进行&运算,获得的结果是hash值的最后4位。运算规则参考我上一篇文章算法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// oldCap = 16
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// newCap =oldCap << 1 = 32
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
···// 省略其余代码,下面就是扩容代码,数据怎么转移到新数组newTab中
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// oldCap是旧桶的个数,也就是没扩容以前数组的长度,这里等于16 。这里遍历桶
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; //清除桶元素,利于回收
// 若是桶中元素后面没有其余元素,直接计算新index,插入新桶中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 若是桶中元素还有下一个元素,节点类型是TreeNode类型,那么按照TreeNode方式插入。这个比较复杂
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 到了这个分支,就说明节点类型是链表,咱们主要分析这个
else {
// preserve order
// 3.1
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// j + oldCap是由于高位,正好是2的4次方,也就是oldCap的长度
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
复制代码
这里注释preserve order说明了下面链表的迁移保持顺序,让我想起来Java 1.7的HashMap在扩容的时候,全部元素所有从新计算Hash&(NewTable.length-1),致使扩容后的链表没有保持顺序,因此在并发的Rehash状况下会致使死循环。shell
来看看核心判断条件(e.hash & oldCap) == 0。这里oldCap=16,就是Hash值与10000进行或运算(只有两个数都为1,结果才为1)。计算结果也就是判断Hash值的第5个bit位是否等于0。这里为何要判断第5个bit位呢?由于以前计算桶下标的时候是直接取的hash值的前4位,若是扩容后,计算t桶下标方式就是(hash & (newCap - 1)) ,newCap =32,也就是hash & (100000-1),也就是与11111进行或运算,获得的结果就是hash值的前5位。看到这里不知道你是否有点迷糊,那就看个图。数组
看到这里,你会发现这个设计很是的巧妙,避免了所有从新Rehash的过程,而且新增的bit为能够认为是随机的,这样子就能够将以前冲突的数据分散到新的桶中。bash
参考部分:并发
HashMap今世前身ide