HashMap
HashMap底层数据结构
底层数据结构:hash表数据结构,即数组+链表|红黑树
- 往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,当出现hash相同的key
- 如果key相同,则覆盖原始值
- 如果key不相同(hash冲突),则将当前数据放入链表或红黑树中
- 获取数据时,对key进行hash运算,找到数组中对象的hash值下标,在进一步判断key是否相同,从而找到对应的值
在 JDK1.8之前, Hashmap使用链表来解决哈希冲突。当哈希冲突较多时,链表中的元素增多,査找、插入和删除的时间复杂度从 O(1)退化为 O(n)。因此在 JDK1.8引入红黑树,将链表长度超过一定國值(链表长度>=8 && 数组长度>= 64)时的链表转换为红黑树,避免性能急剧下降。当链表长度降到6以下时,红黑树会重新退化为链表,保持简单高效。
··
HashMap的扩容操作是怎么实现的?
- 不管是JDK1.7或者JDK1.8 当put方法执行的时候,如果table为空,则执行resize()方法扩容。默认长度为16。
- 扩容因子为0.75,以2的n次方扩容,最高可扩容30次。如第一次是长度达到16**0.75*=12的时候开始扩容,16*2^1=32。
- HashMap 采用2的n次方倍作为容量,主要是为了提高哈希值的分布均匀性和哈希计算的效率。
- Hashmap通过(n- 1)&hash 来计算元素存储的索引位置,这种位运算只有在数组容量是2的n次方时才能确保索引均匀分布。位运算的效率高于取模运算(hash %n),提高了哈希计算的速度。
- 当 Hashap 扩容时,通过容量为2的 n次方,扩容时只需通过简单的位运算判断是否需要迁移,这减少了重新计算哈希值的开销,提升了rehash 的效率
JDK1.7版本扩容
- 先生成新数组;
- 遍历老数组中的每个位置上的链表上的每个元素;
- 获取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标;
- 将元素添加到新数组中去;
- 所有元素转移完之后,将新数组赋值给HashMap对象的table属性。
JDK1.8版本扩容
- 先生成新数组;
- 遍历老数组中的每个位置上的链表或红黑树;
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去;
- 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置;
- 统计每个下标位置的元素个数;
- 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置;
- 如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置;
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性。
··
JDK1.8对红黑树进行的改动
- 改进了哈希函数的计算:(JDK1.7时使用
indexFor
方法确定元素的位置,JDK1.8时采用(n-1) & hash
)JDK1.8 中优化了哈希函数,使得哈希值的分布更加均匀,减少了哈希冲突的发生。通过在生成哈希值时使用“扰动函数”,确保哈希值的高低位都能参与到桶的选择中。 - 扩容机制优化:JDK1.8改进了扩容时的元素迁移机制。在扩容过程中不再对每个元素重新计算哈希值,而是根据原数组长度的高位来判断元素是留在原位置还是迁移到新数组中的新位置。这一改动减少了不必要的计算,提升了扩容效率。
- 头插法变为尾插法:头插法的好处就是插入的时候不需要遍历链表,直接替换成头结点,但是缺点是扩容的时候会逆序,而逆序在多线程操作下可能会出现环,产生死循环,于是改为尾插法。
··
解决hash冲突的四种方法
- 链地址法 (HashMap)
- 再哈希法
- fi=(f(key)+i*g(key)) % m (i=1,2,……,m-1), 其中,f(key) 和 g(key) 是两个不同的哈希函数,m为[哈希表]的长度
- 第二个函数 g(key) 确定移动的步长因子
- 建立公共溢出区
- 基础表和溢出表。将所有关键字通过哈希函数计算出相应的地址。然后将未发生冲突的关键字放入相应的基础表中,一旦发生冲突,就将其依次放入溢出表中即可。
- 开放定址法
- 线性探查 - 发生冲突后,顺序查找下一个空余的槽位
- 二次探测 - 左右探测空余的槽位
- 伪随机探测 - 这种方法即是产生一些随机系列值,并给定随机数作为起点。