HashMap 在1.8做了哪些优化
1. 概述
嗨,大家好,我又来了。今天分享的面试合集是
HashMap 在1.8做了哪些优化
。HashMap是面试高频题,我也会从源码的角度逐一解析。有什么不对的,希望大家及时指正。
2. 数据结构
2.1 jdk1.7版本体现
使用数据结构是数组 + 链表
第一步:执行put方法
// --------------- 此方法为 put方法 ------------------
// 通过hash值以及数组的长度计算出该值,在数组中对应的位置
int i = indexFor(hash, table.length);
// 如果是第一个添加的话 table[i] 一定是null。 所以这个for不会进去
for (Entry<K,V> e = table[i]; e != null; e = e.next)
// 开始添加节点
addEntry(hash, key, value, i);
第二步:执行addEntry方法
// --------------- 此方法为 addEntry方法 ------------------
// 如果是第一次执行的话 下面这个if一定不会满足
if ((size >= threshold) && (null != table[bucketIndex]))
// 创建节点
createEntry(hash, key, value, bucketIndex);
第三步:执行createEntry方法
// --------------- 此方法为 createEntry方法 ------------------
// 如果第一个执行的话,得到的e 一定是null。
Entry<K,V> e = table[bucketIndex];
// 所以在对应数组table的位置【bucketIndex】 插入一个节点
// 如果下次执行的话,会将该节点插入到链表头部(因为此处代码将链表作为将添加节点的next节点)
table[bucketIndex] = new Entry<>(hash, key, value, e);
第四步:查看class【Entry】结构
// 可以发现Entry 本身就是一个链表结构
static class Entry<K,V> implements Map.Entry<K,V> {
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
2.2 jdk1.8 版本体现
使用数据结构是数组 + 链表 + 红黑树
第一步:putVal 实现逻辑
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 如果说数组下标位置为null的话。说明该索引位置没有链表
if ((p = tab[i = (n - 1) & hash]) == null)
// 创建一个新的链表,直接赋值到索引位置
tab[i] = newNode(hash, key, value, null);
// 执行到此处的时候,挨个遍历节点
for (int binCount = 0; ; ++binCount) {
// 直到遍历到最后一个节点
if ((e = p.next) == null) {
// 直接插入到尾部
p.next = newNode(hash, key, value, null);
// 如果count >= 7的话 将整个链表 转换为二叉树
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;
}
}
第二步:核心点
// 如果count >= 7的话 将整个链表 转换为二叉树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
上述的条件其实是链表转换二叉树的核心点。
3. 插入方式
3.1 jdk1.7版本体现
链表插入方式链表 头插法
第一步:方法【createEntry】
详细的执行流程可以参照2.1 的过程。
void createEntry(int hash, K key, V value, int bucketIndex) {
// 此时将数组的中指定下标的链表取出。所以此时e 其实就是一个链表
Entry<K,V> e = table[bucketIndex];
// 添加新的节点。将链表e作为next节点。
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
综上源码所述,使用的是头插法
3.2 jdk1.8 版本体现
链表插入方式链表 尾插法
其实就是添加到链表的尾部
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 此代码是尾插法的核心部分
p.next = newNode(hash, key, value, null);
}
p = e;
}
4. 获取索引方式
4.1 jdk1.7 版本体现
在1.7版本中 只是将hash值 以及数组长度进行&运算,从而计算出索引位置。
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
4.2 jdk1.8 版本体现
Java1.8的hash()中,将hash值高位(前16位)参与到取模的运算中,使得计算结果的不确定性增强,降低发生哈希碰撞的概率
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5. 扩容优化
5.1 jdk1.7 版本体现
jdk1.7中 使用2倍扩容 以及rehash算法来将旧数组上的元素 添加到新数组中
第一步:方法addEntry 扩容机制
// --------------- 此方法为 createEntry方法 ------------------
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
}
}
第二步:方法resize核心功能
// --------------- 此方法为 resize核心功能 ------------------
void resize(int newCapacity) {
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
}
第三步:方法transfer 开始移动元素
// --------------- 此方法为 transfer方法 ------------------
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历旧数组
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 根据新的hash值 以及数组长度 计算位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
5.2 jdk1.8 版本体现
扩容也是2倍的扩容方式。
JDK 1.8 在扩容时并没有像 JDK 1.7 那样,重新计算每个元素的哈希值,而是通过高位运算(e.hash & oldCap)来确定元素是否需要移动
do {
next = e.next;
// 如果结果不是1的时候 代表需要移动了
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 (hiTail != null) {
hiTail.next = null;
// 当前下标的长度 + 旧数组长度
newTab[j + oldCap] = hiHead;
}