版权声明:本文章原创于 RamboPan ,未经允许,请勿转载。
无意看到了 HashMap 的扩容机制,想了挺久,才逐渐明白,感觉很精髓。
简单分享下自己理解的扩容过程。源码版本为 Android 28 JDK 1.8。
先简单说说 HashMap 结构,大家都知道它的内部存储结构是由一个数组进行散列存储。
如果把 n 个对象放入 n + 1 个格子里,就算 n 个平均散列了,第 n + 1 个肯定会放入 n 个格子中一个。
那既然会出现冲突的情况,肯定需要解决。 HashMap 采用的是链表的方式。
static class HashMapEntry<K, V> implements Entry<K, V> {
//键
final K key;
//值
V value;
//散列值
final int hash;
//下一个节点的引用
HashMapEntry<K, V> next;
}
从 HashMapEntry 成员变量能看出,存了下一个 HashMapEntry 的引用,没有看到其他的逻辑。
既然这样,那就先来看看它是怎样扩容的。doubleCapacity() 中是怎么处理的。
private HashMapEntry<K, V>[] doubleCapacity() {
//新建一个关于旧数组的引用
HashMapEntry<K, V>[] oldTable = table;
//获取旧数组的长度
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
//两倍扩容
int newCapacity = oldCapacity * 2;
//生成新数组
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
//大佬说这是最优雅的地方,我们来瞅瞅。
for (int j = 0; j < oldCapacity; j++) {
/*
* Rehash the bucket using the minimum number of field writes.
* This is the most subtle and delicate code in the class.
*/
//从旧数组的 0 位置开始查找,空则不考虑。
HashMapEntry<K, V> e = oldTable[j];
if (e == null) {
continue;
}
//每个节点的哈希值与旧容量按位进行运算,得到高位。
//e.hash 代码【3】 highBit //代码【6】
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
//根据高位与第 j 个位置进行或计算,得到新数组位置。
newTable[j | highBit] = e;
//查找该节点的下一个 HashMapEntry,并且判断在新数组哪个位置。
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash & oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j | nextHighBit] = n;
else
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}
首先说说 代码【3】处,e.hash 是什么。
看看 HashMapEntry 的构造函数。
HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
构造函数只有一个,hash 值是在初始化时传递的。
那么我们就想想什么时候初始化,那应该就是放数据的时候,肯定是发生在加入新的节点时。
public V put(K key, V value) {
……
//看名字仿佛是散列过两次。
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
//能看出来是从这添加一个新的节点。
addNewEntry(key, value, hash, index);
return null;
}
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
果然,就是在这里进行的初始化,就是使用传入的 hash 值,这个 hash 值又是在 secondaryHash() 中得到的。
那么我们去看看 secondaryHash();
public static int secondaryHash(Object key) {
return secondaryHash(key.hashCode());
}
//第一次 key.hashCode() ,是使用 Object 中的 hashCode();
//作用是针对键进行散列,使得算出来的 hashCode 更唯一性。
public int hashCode() {
int lockWord = shadow$_monitor_;
final int lockWordMask = 0xC0000000; // Top 2 bits.
final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
if ((lockWord & lockWordMask) == lockWordStateHash) {
return lockWord & ~lockWordMask;
}
return System.identityHashCode(this);
}
//第二次的 secondaryHash(),是使得对散列过的键值,更均匀的散列在散列表中。
//进行了大量的 移位 与 异或计算,移位使得 高位 与 低位 能更合理的对散列均匀。
private static int secondaryHash(int h) {
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
从上面的分析可以看出,最后进行初始化的 HashMapEntry 中的 hash 值,就是二次散列后的结果。
那么放在散列中的哪个位置呢,就是对应的 index ,index = (table.length - 1) & hash 进行计算得出的。
那么简单画个示意图说明下,只分析了低 8 位的情况。
那么存放新节点基本上就这样了,没有分析如果存放出现冲突的情况,不是本文分析的重点。
下面接着 代码【2】中下半部分继续说,即代码【6】。
for (int j = 0; j < oldCapacity; j++) {
HashMapEntry<K, V> e = oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash & oldCapacity;//highBit 代码【6】
HashMapEntry<K, V> broken = null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash & oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j | nextHighBit] = n;//代码【8】
else
broken.next = n;//代码【10】
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
从 0 开始从旧数组中读取数据,先说说 highBit,高位与旧容量进行与计算,因为容量都是 2 的指数,以 8 为例子。
假设旧数组中第一个节点中的 hash 值为 11110000,与 8 - 1 进行 & 计算时为 0 ,0 与 0 进行 | 运算,所以 index 为 0。新数组中还是 0 ,位置没变。
newTable[ j | highBit ] —> newTable[ 0 | 0] —> newTable[0]。
不过如果 第一个节点中的 hash 值为 11111000,那么此时的 highBit 就为 1 换为 10 进制就是 8 。1 再与 8 进行 | 运算,就为 9 了。
newTable[ j | highBit ] —> newTable[ 1 | 8 ] —> newTable[9]。
此时发现位置不一样了是吧。不过光看代码不能感觉什么,如果能想下实图的话那就明白了。
那上个图来说明下,我们只分析了两种情况,其他节点类似,循环的最后一个节点进行判断后结束。
忘了说明:新数组的 此时 index 计算 是当它再次扩容时需要计算时 table.length - 1 = 15 。
所以就是后四位。这次扩容是 oldTable.length - 1 = 7 ,所以是后三位。
是不是发现扩容其实更均匀的把以前一个格子的节点,再分散的新的数组。是不是很机智。
那么肯定要说了,这只是简单地说了,如果每个格子只有一个节点,那么有多个节点呢,是单链表结构呢 ?
那我们继续分析 if (broken == null) 与 if (broken != null) 的情况。
HashMapEntry<K, V> broken = null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
//查找下一个节点
int nextHighBit = n.hash & oldCapacity;
//判断该节点高位,如果高位与之前相同,那么查找下一个。
//如果该节点高位不同,则判断 断点是否为null。
if (nextHighBit != highBit) {
//如果为null,则为第一次,需要移动的另外的格子(类似上图中,旧数组:1 -> 新数组:9)
if (broken == null)
newTable[j | nextHighBit] = n;
//如果不为null,则证明之前已经移动过,那么此时需要移动的,就在之前的断点之后继续添加。
else
broken.next = n;
//重置断点位置与高位。
broken = e;
highBit = nextHighBit;
}
}
//最后判断断点是否为 null,为 null 时将下一个 HashMapEntry 引用置为 null。
if (broken != null)
broken.next = null;
主要问题: broken 为什么会切换 ?怎么切换 ?切换之后又是怎样保证在新的位置上形成链表 ?
有时可以先根据结果猜想怎么的条件触发,再反推原理,这样比较好设计的初衷。
只分析一个节点,其他节点类似。假设 oldTable[0] 处节点,单链表长度为 4。1111 0 000(因为此时只需要关心低4位,所以我间隔开了)。
对应代码:
for (int j = 0; j < oldCapacity; j++) {
……
// 根据之前的分析,高位为 0
int highBit = e.hash & oldCapacity;
// j 为 0 , newTable[ 0 | 0 ] = newTable[0]
newTable[j | highBit] = e;
……
}
HashMapEntry<K, V> broken = null;
//判断下一个节点是否为 null ,此时不为 null ,对第二个节点 1111 1 000 进行判断。
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
//计算 nextHighBit
int nextHighBit = n.hash & oldCapacity;
//上一步 highBit = 0 ,nextHighBit = 1 , broken == null,满足判断。
if (nextHighBit != highBit) {
if (broken == null)
//此时 newTable [ 0 | 8] = newTable[8]
newTable[ j | nextHighBit ] = n;
else
broken.next = n;
// broken 指向第一个节点 1111 0 000
broken = e;
//高位改为 1
highBit = nextHighBit;
}
}
//再循环判断没有下一个,退出循环,将 第三个节点 .next 置为 null。
//如果 broken == null ,说明单链表在移动时都移动到新数组同一个位置,所以 断点 为 null ,也不需要置 null。
if (broken != null)
broken.next = null;
//判断下一个节点是否为 null ,此时不为 null ,对第三个节点 1010 1 000 进行判断。
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
//计算高位
int nextHighBit = n.hash & oldCapacity;
//此时 highBit 为 1 , nextHighBit 也为 1 ,说明不处理,直接跳过,判断第四个节点。
if (nextHighBit != highBit) {
……
}
}
//对第四个节点 1011 0 000 进行判断。
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
//计算 nextHighBit = 0
int nextHighBit = n.hash & oldCapacity;
//上一步 highBit = 1 ,nextHighBit = 0 , broken != null,满足判断。
if (nextHighBit != highBit) {
if (broken == null)
newTable[ j | nextHighBit ] = n;
else
// broekn 为第一个节点。
broken.next = n;
// broken 指向第三个节点
broken = e;
// highBit 改为 0
highBit = nextHighBit;
}
}
broken 的意义,就是存储某一边末端节点的引用:
以上分析仅供参考,如果有错误,希望指出,共同进步。