一. resize()扩容方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //当前hash桶
//当前hash桶的大小
int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取原始HashMap数组的长度。
int oldThr = threshold; //容量扩展的临界值
//初始化新的hash桶的大小和hashMap阈值
int newCap, newThr = 0;
if (oldCap > 0) { //如果当前hash桶的大小大于0
if (oldCap >= MAXIMUM_CAPACITY) { //如果当前hash桶的大小到达最大值,不再进行扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果不超过最大值,它将被扩展为原始值的两倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果旧的hash桶大小 >= 16,那么新的hashMap阈值也翻倍
newThr = oldThr << 1; // double threshold
}
//如果当前hash桶大小为 0,但是阈值不为 0,那么表示初始化的HashMap对对象进行扩容
else if (oldThr > 0)
//将当前hashMap的阈值赋值给新hash桶的大小,此时阈值和hash桶大小一致
newCap = oldThr;
else { //如果当前hash桶的大小和hashMap阈值都是0,则使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; //设置新hash桶大小为默认值 16
//设置新hashMap阈值为 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { //如果当前hashMap的阈值为 0,则根据当前hash桶的大小和负载因子计算新的阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //更新阈值
//根据新hash桶的大小构建新的Node数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; //将新构建的Node数组赋值给table
//遍历存储桶
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e; //定义节点e用来指向待转移的节点
if ((e = oldTab[j]) != null) {//如果当前下标为 j 的桶中没有元素,直接结束
oldTab[j] = null; //释放原始表地址
//如果当前桶的中只有一个节点,直接计算出该key在新hash桶中对应的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e; //这里又一次使用了 & 代替取余算法
else if (e instanceof TreeNode) //如果当前节点存在于树中而不是链表中
//采用红黑树的转移逻辑
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //如果当前节点存在于链表中而不是树中
//定义低位链表的头尾节点
Node<K,V> loHead = null, loTail = null;
//定义高位链表的头尾节点,其中高位链表在hash桶中的下标比低位节点大oldCap
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next; //定义临时节点
do {
//新表的容量是旧表的两倍。单个链表分为高位链表和低位链表。
next = e.next;//使用next指针指向链表下一个节点,防止下一个节点丢失
//低位链表,关注的对象是oldCap,而不是oldCap-1
if ((e.hash & oldCap) == 0) { //利用 & 运算得出key落在低位链表中
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); //循环直到当前hash桶对应下标中没有元素为止
if (loTail != null) {//如果低位链表不为空,将低位链表赋值给低位hash桶中
loTail.next = null;
newTab[j] = loHead;
}
//高位链表放置在新表中,索引=原始索引+ oldCap
if (hiTail != null) {//如果高位链表不为空,将高位链表赋值给高位hash桶中
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
扩容流程概括:
- 计算新hash桶的大小和阈值
- 根据新hash桶的大小生成新的hash桶数组,如果当前hash桶为空,构造一个长度默认的hash桶
- 将新hash桶以及新hash桶的大小以及阈值设置到当前HashMap对象
- 对当前hash桶中的元素进行转移
- 遍历hash桶
- 遍历指定下标hash桶中的待转移节点
- 如果指定下标hash桶中待转移节点只有一个,直接计算在新hash桶中的落点并转移到新hash桶中
- 如果指定下标hash桶中存储的是树,按照树的结构来转移(暂不做介绍)
- 如果指定下标hash桶中存的是链表
- 创建低位链表头尾指针和高位链表头尾指针
- 将待转移元素按照尾插法插入到低位链表和高位链表中
- 将低位hash桶和高位hash桶分别指向低位链表和高位链表
- 返回新的hash桶
从resize()的实现中可以看出,如果是多节点链表,则将生成高位链表和低位链表,即(e.hash & oldCap) == 0是低位链表,(e.hash & oldCap) != 0是高低链表。
为什么将列表分为高位和低位?
想象一下,如果所有索引都是使用下标=(hash &(新制表符长度-1))计算出来的,这是因为它们基于下标存储,从而导致了额外的时间(寻址等)或空间(辅助参数,结构,索引冲突时的开销),这也是一种出色的优化技术,可以先保存数据,然后搜索附加项。
二. put(k , v )添加
public V put(K key, V value) {
//先调用hash(key)方法获得key的hash值,在调用putVal方法插入数据,并返回结果
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //定义临时变量
if ((tab = table) == null || (n = tab.length) == 0) //表未初始化或长度为0,hash桶为空。
n = (tab = resize()).length; //扩容并将扩容后hash桶的大小赋值给n
//(n - 1) & hash确定元素存储在哪个存储桶中,该存储桶为空以及将新生成的节点放置在存储桶中
//在这种情况下,该节点放置在数组中
if ((p = tab[i = (n - 1) & hash]) == null) //如果key对应hash桶的位置上没有节点
//将put进来的key,value生成一个Node节点,并将该节点赋值给hash桶指定下标上
tab[i] = newNode(hash, key, value, null);
else { //如果key对应hash桶的位置上有节点
Node<K,V> e; K k; //定义临时变量
//比较值区中第一个元素(数组中的节点)的哈希值是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //将头节点赋值给临时变量p,用于下面返回
else if (p instanceof TreeNode) //哈希值不相等,即key不相等;它是一个红黑树节点。
//调用红黑树的putTreeVal方法将key,value设置到红黑树中
//并且如果红黑树中存在该key,将对应的value赋值给e
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //对于链表节点
//累计便利次数,用于在下面链表长度达到 8 的时候转成红黑树
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //如果链表中不存在下一个节点
p.next = newNode(hash, key, value, null); //在尾部插入一个新节点
//如果便利次数达到 7 次,那么插入新节点后链表长度将达到 8
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; //用于遍历存储桶中的列表,结合前一个e = p.next,可以遍历列表
}
}
if (e != null) { //如果e != null,表示链表(红黑树)中存在相同的key
V oldValue = e.value; //记录旧值,用于返回
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //访问后回调
return oldValue; //返回旧值
}
}
//能走到这一步,在上面if (e != null)判断中 e == null
//表示新增了元素而不是替换了元素
//modCount记录了HashMap在节点数量上变化的次数,在这里加一
++modCount;
if (++size > threshold) //当实际大小大于阈值时,将扩大容量。
resize();
afterNodeInsertion(evict); //插入后的回调
return null; //由于是新增节点,没有覆盖旧节点的值,返回null
}
final void treeifyBin(Node<K,V>[] tab, int hash) { //将链表转换为红黑树
int n, index; Node<K,V> e;
//如果地图的容量小于64(默认值),则会调用resize()扩容,并且不会将其转换为红黑树。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab); //调用TreeNode的树排序方法
}
}
扩容流程:
- 计算需要put的key的hash值
- 判断hash桶是不是空,为空先进行扩容
- 判断该key值对应在hash桶上是否存在节点,不存在则直接在hash桶中创建节点
- 对比头节点的hash值和key是否和需要查询的key一致,如果一致直接覆盖头节点
- 判断头节点在红黑树中还是链表中
- 如果在红黑树中,则在红黑树中查找该节点,如果在链表中,则遍历链表查询该节点
- 如果在链表(红黑树)中存在节点的hash值和key和需要put的key一致,进行覆盖操作
- 如果不存在,创建新的节点,添加到链表(红黑树)中
- 如果当前HashMap中元素数量超过阈值,进行扩容
- 如果put()方法覆盖了某个节点,则返回这个节点的value,否则返回null
索引计算:
计算索引时,此值必须在[0,length]的左右闭合间隔内。基于此条件,例如,默认表长度为16,用公式(n 1)和哈希代替,结果必须在[0,length]范围内。这里还有一个小技巧。在容量必须为2 ^ n的情况下,H&(长度1)= h%长度。这里使用位运算的原因是,位运算由计算机直接处理,效率高于%运算。
红黑树的转换:
在put方法中,当列表的长度大于(TREEIFY_THRESHOLD-1)时,逻辑将转换为红黑树。实际上,这只是初步判断。在转换后的方法treeifyBin()方法中,将对制表符长度进行第二次检查。
put流程图:
三. HashMap中使用的哈希算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
此哈希首先将key向右移动16位,然后与key进行XOR。由于int只有32位,因此16位的无符号右移等效于将高位的一半移到低位:
这样,可以避免仅由低级数据计算散列引起的冲突。计算结果由高位和低位数据的组合确定,可以避免哈希值的不均匀分布。而且,位操作更有效。
这涉及put方法中的另一操作。
tab[i = (n - 1) & hash]
ab是两个表,n是映射集的大小,hash是上述方法的返回值。由于通常不指定map集合的大小或在初始化时创建大型map对象,因此基于容量大小和键值的哈希算法只会在开始时计算低位。尽管开始时容量的二进制高位全为0,但是key的二进制高位通常很有价值,因此在哈希方法中首先使用key.hashCode的右移与其自身相差16位,这使较高的位置参与了hash,并在更大程度上降低了冲突率。
这里将不再展开,为避免篇幅过长,将hashmap的remove(),get()等常用方法,以及常见问题和1.7与1.8的改动写到下面这篇↓
参考链接:
链接: HashMap源码分析,基于1.8对比1.7
链接: JDK1.8 HashMap源代码分析