HashMap的源码分析

HashMap的源码分析,第1张

HashMap的源码分析

文章目录

前言一、HashMap重要的几个属性二、HashMap源码分析

1. HashMap的初始化2.HashMap容量为什么要设置为2的幂次方整数3. HashMap的哈希算法4. HashMap添加元素的过程5. HashMap的扩容机制 JDK1.7 中HashMap添加元素导致死循环1.8 对HashMap的改进HashMap和HashTable的区别


前言

HashMap实现于Map接口,是一种存储key-value对的容器。
底层数据结构为哈希表,数组,链表,红黑树(当链表的长度大于等8时,链表会转换为红黑树结构)。
数组和链表都有其各自的优点和缺点:
数组连续存储,寻址容易,插入删除 *** 作相对困难;
链表离散存储,寻址相对困难,而插入删除 *** 作容易;
而HashMap结合了这两种数据结构,保留了各自的优点,又弥补了各自的缺点。


一、HashMap重要的几个属性

在了解HashMap底层实现原理之前,需要对HashMap中几个重要的属性有一定的认知,具体属性如下:

二、HashMap源码分析 1. HashMap的初始化

HashMap一共提供了三个构造方法,无参构造,指定初始容量和指定初始容量及负载因子的构造方法。

无参构造:所有的属性都取默认值,如loadFactory = 0.75F,默认的初始容量为DEFAULT_INITIAL_CAPACITY即16。指定初始容量:负载因为为默认的0.75F,而指定初始容量之后,HashMap并没有直接将指定的初始容量作为真正的HashMap的容量,而是经过
计算获取一个比给定整数大或者等于的最接近的2的幂次方整数,如给定3,则设置为4,给定9,则设置为16。指定初始容量及负载因子:其实该构造方法并不常用,因为负载因子的设定关系到扩容的时机,负载因子设置过大会导致HashMap的hash冲突比较严重,影响查询的效率,设置过小,会导致频繁地进行扩容,影响添加元素的效率。

2.HashMap容量为什么要设置为2的幂次方整数

HashMap其实就是哈希表,而哈希表数据结构的优点就是存储元素和查询元素的效率特别高,在不考虑hash冲突的情况下时间复杂度可以降为O(1),
所以为了避免hash冲突,HashMap内部进行了很多处理,如容量设置为2的幂次方整数,具体见:HashMap的tableSizeFor。


关于上述源码即可以拿到大于给定的cap最接近的2的幂次方整数。

关于HashMap的容量为什么要设置为2的幂次方整数,其实具体原因需要结合HashMap在put元素的 *** 作,HashMap在putVal的时候需要计算table的index,
即entry桶在table的索引,计算index的原理就是利用(capacity -1) & hash,当容量capacity设置为2的幂次方时,(capacity-1)之后的二进制码最高位之后可以保证全是1,如:

容量为8, 对应的7 的二进制码为0000 0111容量为16,对应的15的二进制码为0000 1111

这样对hashCode进行按位与&计算之后可以保证结果既可以是基数也可以是偶数,而且结果和hashCode的后几位有关系,只要对应的key的hashCode算法足够好,就可以有效减少hash碰撞的几率。

HashMap在初始化建议根据实际情况设置初始容量大小,指定初始化容量可以有效减少HashMap的扩容resize次数,因为在进行扩容时会导致重建hash表,重建hash表需要重新计算这些数据在新table数组中的位置并进行复制处理,实际上HashMap会采用第一个大于指定数值的2的幂作为初始化容量。

3. HashMap的哈希算法

在HashMap添加元素的时候需要根据key计算对应的hashCode然后再确定该元素对应的数组下标,计算下标的算法为(capacity -1) & hash,其中capacity设置为2的幂次方整数是为了减少hash冲突的第一步,在HashMap的哈希算法中对key的hashCode也进行了一些处理,HashMap的hash算法源码如下:

引用自HashMap的hash算法

为了减少hash碰撞的几率,HashMap并不是直接用key.hashCode()作为hash值,而是通过上述的hash(Object key)算法将
hashcode 与 hashcode的低16位做异或运算,混合了高位和低位得出的最终hash值,冲突的概率就小多了。

举个例子:

有个蒸笼,第一层是猪肉包、牛肉包、鸡肉包,第二层是白菜包,第三层是豆沙包,第四层是香菇包。这时你来买早餐,你指着第一层说除了猪肉包,随便给我一个包子,
因为外表无法分辨,这时拿到猪肉包的概率就有1/3,如果将二层、三层、四层与一层混合在一起了,那么拿到猪肉包的概率就小多了。

我们的hash(Object key)算法一个道理,最终的hash值混合了高位和低位的信息,掺杂的元素多了,那么最终hash值的随机性越大,
而HashMap的table下标依赖于最终hash值与capacity-1的&运算,这里的&运算类似于挑包子的过程,自然冲突就小得多了。

计算过程如下:

这样的过程叫做扰动函数。

4. HashMap添加元素的过程

HashMap对table的初始化是在第一次put元素的时候,HashMap进行put元素的过程如下:

    table为空,为空则调用resize()方法初始化table数组。table不为空,则根据key的(table的长度-1) & hashCode定位到table数组对应的元素

    如果table对应的元素为null,则直接放置在table对应的位置如果table对应的元素不为null,这时候说明产生了hash碰撞,而HashMap对hash碰撞的处理方式是利用链表或者红黑树,
    当table对应的元素为TreeNode,则放置到红黑树对应的节点,如果不是红黑树结构,这时候就需要遍历链表

    遍历链表时发现对应的key已经存在,则进行value覆盖遍历链表时发现对应的key不存在,则新建一个节点追加到链表的尾部,当追加后的链表长度大于树形阈值TREEIFY_THRESHOLD,则将链表转换为红黑树。 添加元素后如果table的长度大于扩容的阈值threshold,则进行resize扩容。

5. HashMap的扩容机制

HashMap扩容的时机取决于扩容阈值threshold,阈值的计算规则为threshold = capicaty * loadFactor,
当HashMap的size大于或等于 threshold则HashMap会进行扩容处理。

JDK1.7 中HashMap添加元素导致死循环

1.7 中 HashMap的链表扩容时转移Entry上的链表时采用的是头插法,多线程的情况下可能会产生链表的闭环,导致死循环。
假如现在有两个线程,put元素时产生了HashMap的扩容机制,流程如下图:

1.8 对HashMap的改进

1.7 中 HashMap的底层实现是数组 + 单链表,1.8底层实现增加了红黑树,当链表的长度大于等于8(默认)时将链表转换为红黑树。1.7 中 HashMap的链表使用头插法,多线程的情况下可能会产生链表的闭环,导致死循环,1.8之后,链表添加元素使用尾插法。 HashMap和HashTable的区别

HashTable:
- 继承于Dictionary类
- 是线程安全的,几乎所有的方法都加上了synchronized
- key 和 value 都不允许为 null
- 默认的初始容量为 11,负载因子为0.75
- 扩容机制为原容量的两倍 + 1

HashMap:
- 继承于 AbstractMap类
- 非线程安全的
- key 和 value 可以为 null
- 默认的初始容量为16,负载因子为0.75,扩容的阈值为capacity * loadFactor
- 扩容机制为原容量的两倍

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5707434.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存