编辑
2025-08-10
个人笔记
00

首先,HashMap的主要结构是由 Node 数组(哈希桶)+ 链表/红黑树(哈希冲突)所实现的

这里我们通过对 HashMap 的put方法来缕清 HashMap 的主要结构。HashMap的put方法主要逻辑可以分成三个大部分:

  • 初始化检测
  • Key-Value 节点插入
    • 索引计算
    • 哈希冲突处理
    • 链表 / 红黑树转换
    • 相同Key-Value更新
  • 扩容检测

首先是边界检测部分,HashMap 在进行插入节点之前要通过比对Node数组length == 0isNull情况来判断是否需要初始化,如果需要初始化就调用resize()函数进行初始化容量

检测完毕后就进入插入环节,计算插入对象的哈希索引,通过对象的hashCode()方法计算出hashCode(大部分类继承用Object类的hashCode方法用对象地址计算,String是用字符串内容计算,数字类有不同的实现)

然后将 hashCode 二进制展开,将其右移16位与再与本身进行与运算,从而将高16的信息融入到低16位中

最后再用HashMap的容量 - 1与前面异或运算结果进行与运算。这是因为HashMap的容量是2的幂次方,将其减去一进行二进制展开就可以得到幂次方减去一位的全1二进制数,这样进行与运算我们就相当于每一次扩容就多读取一位与运算结果的信息,并且我们可以根据新增读取的位是1或0来决定是否要迁移对应位置的数据。

但是通过按哈希表大小来读取hashcode信息这样会容易遗漏高16位的信息,而前面(h = key.hashCode()) ^ (h >>> 16)操作能尽量让高16位的信息也被读取,从而让哈希函数更均匀分布

然后在上面一系列的操作后,我们找到对应的Node数组位置,进行节点插入,如果当前槽位是空就new Node放入对应Key-Value

如果哈希冲突就通过拉链法用尾插法插入新节点,1.8之前用的是头插法,多线程环境下HashMap多个线程扩容可能会导致链表成环。而且尾插法能保证扩容前后元素位置一致,且尾插法的顺序更符合红黑树结构。

当链表长度大于8时,且HashMap容量大于64时,链表就会被转换为红黑树,而红黑线小于6时就回转换回链表。这是由于链表只需要一个指针指向下一个节点就好了,而红黑树是一种二叉搜索树,相同的数据需要两个指针指向左右子树,占用更大的内存

如果插入发现是相同的Key则回替换原有的Value,其他不变

最后put会进行HashMap的容量检测,如果数组的使用率大于装填因子0.75就会进行一次扩容操作。0.75是通过泊松分布用概率学计算出来的最佳装填因子大小,如果过小会导致数组使用率过低浪费内存,过大则会导致哈希冲突频繁