【八股文】HashMap小知识

164 阅读4分钟

一、HashMap计算链表索引原理

1.Hash值计算

取HashKey对象的hashCode值,高十六位维持不变 低十六位与高十六位进行异或运算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.HashTab数组计算索引
if ((p = tab[i = (n - 1) & hash]) == null)

这里的n在HashMap中一定是2的n次幂 满足这个条件下

(n - 1) & hash 等价于 hash % n

这里给出推导,因为位运算中 >> 1相当于/2 而因左移而丢失的部分则是余数

如 1111 1001 这个二进制为249 左移动4位 相当于 / 2的4次方(16) 1111 1101 >> 4 = 1111 即15 那么余数恰好就是左移后丢失的1001,反推1111 << 4 = 1111 0000 此时相当于 15 * 16 = 240 这时候需要加上余数1001(十进制9)才刚好等于1111 1001(十进制249)。而想要获取到这后4位(余数)则需要4位 1111 与hash进行与运算,而1111则恰好是16 - 1 。这也要求了n必须是2的次方幂

PS: 至于为何上文中采用h = key.hashCode()) ^ (h >>> 16) 计算 hash值
原因是 (n - 1) & hash 中 (n-1)只有低位的几个位,而高位则不会参与到 与 运算中,为了减少hash冲突,则将高位移至低位,与低位进行异或运算,使hash运算中高低位都能参与到计算中来,减少hash冲突的可能性。使得散列的更均衡。

二、 HashMap 存储

1.HashMap存储结构

HashMap的存储以 数组 + (链表 | 红黑树) 数组 + 链表 结构存储具体如下,node数组存储链表头节点,节点之间根据成员对象next引用形成链表

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}
2.链表转红黑树

通过hash运算定位到数组对应索引上,若存在hash冲突则以链表形式保存起来。 若新增元素后(put方法)后在链表长度达到8(并且map总容量达到64)时链表转换为红黑树存储

3.扩容

HashMap 指定容量时 若设定的容量不是2的n次幂则向上取整到最近的2的n次幂,若不设置则取默认16为长度
HashMap存在一个参数 负载因子默认为0.75,代表若HashMap中实际存储的数据量达到了容量 * 0.75则进行扩容,扩容到当前的两倍。
扩容时,若存储的节点为链表节点,则进行位运算, 旧容量 & hash 因为容量是2的n次幂,则只有一个二进制位为1,做旧容量 & hash == 0 将链表分为两条为链表,分别是高位链表、低位链表,低位链表在原位置,而高位链表在新数组则在原位置 + 旧容量的位置,原因是因为 (新容量 - 1) 和 原来的 (旧容量 - 1)之间只是多了一个1,即若原来容量为 8 则 8 - 1 = 0000 1111 新容量为 16 则 16-1 = 0001 1111 即只有从左到右的第四位不同,多的这一位 0001 0000 则就是旧容量的二进制,hash值与这个数相与,若为0则代表 (新容量 - 1) & hash和(旧容量 -1) & hash是没区别的,因此不变,而若旧容量 & hash != 0 则代表这个位在新的容量下计算的值为1,即原位置 + 旧容量就是新位置的索引。

4.红黑树转链表

红黑树在几个场景下会转换回链表
第一种情况,就是扩容造成的红黑树节点减少,此时若扩容后红黑树节点小于等于6个则退化为链表。
第二种情况,则无关红黑树节点数量,调用remove时,检查红黑树状态(这里指红黑树没做移除动作前),若根节点的左孩子或者右孩子或者左孩子的左孩子(左孙子)为null,这时候需要将红黑树转换为链表。

5.put和putAll扩容区别

put方法先插入再扩容

putAll先扩容再插入

三、题外小知识

1.旧版本HashMap死循环问题

在jdk1.7版本下HashMap插入链表是头插法插入链表(jdk1.8采用尾插法),在并发情况下,触发扩容,则可能会造成链表引用关系成环,造成死循环。