赞
踩
嗨,大家好,我又来了。今天分享的面试合集是
HashMap 在1.8做了哪些优化
。HashMap是面试高频题,我也会从源码的角度逐一解析。有什么不对的,希望大家及时指正。
使用数据结构是数组 + 链表
第一步:执行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;
}
}
使用数据结构是数组 + 链表 + 红黑树
第一步: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);
上述的条件其实是链表转换二叉树的核心点。
链表插入方式链表 头插法
第一步:方法【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++;
}
综上源码所述,使用的是头插法
链表插入方式链表 尾插法
其实就是添加到链表的尾部
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 此代码是尾插法的核心部分
p.next = newNode(hash, key, value, null);
}
p = e;
}
在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);
}
Java1.8的hash()中,将hash值高位(前16位)参与到取模的运算中,使得计算结果的不确定性增强,降低发生哈希碰撞的概率
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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; } } }
扩容也是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; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。