前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java之HashMap详解

Java之HashMap详解

作者头像
阿珍
发布2024-11-13 16:09:17
400
发布2024-11-13 16:09:17

在项目开发中,HashMap是及其常用的数据结构。今天,我们一起来看看它的源码实现(本文源码来自JDK 1.8)。

HashMap有如下类注释:

image.png
image.png

从中可知:

  • 基于哈希表的Map接口实现
  • 允许空值和空键
  • HashMap类大致相当于Hashtable,不同之处在于它是不同步的,是线程不安全的
  • HashMap不保证映射的顺序
  • 为基本操作(get和put)提供O(1)的性能
  • 集合视图进行迭代所需的时间,与HashMap实例的容量加size成正比
  • HashMap实例有两个影响其性能的参数:初始容量和负载因子
  • 当哈希表中的条目数超过负载因子和当前容量的乘积时,将对哈希表进行重建,使哈希表的桶数大约增加一倍。

1 数据结构

HashMap是Map接口基于哈希表的一种实现。

image.png
image.png

哈希表基于数组实现,元素是Entry对象。HashMap中将Entry形成链表(或者红黑树),来解决哈希冲突。 HashMap主要属性如下:

代码语言:javascript
复制
java 代码解读复制代码// 默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量,2^31
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表树化时的阈值,当向entry数量为8的桶put元素时,将引起链表树化
static final int TREEIFY_THRESHOLD = 8;
// 桶取消树化时的阈值
static final int UNTREEIFY_THRESHOLD = 6;

// 哈希表
transient Node<K,V>[] table;
// 哈希表尺寸
transient int size;
// 触发下次扩容时map中的entry数,取值为capacity * load factor,
int threshold;
// 负载因子
final float loadFactor;

// 对HashMap实例的修改次数
transient int modCount;
// Entry视图
transient Set<Map.Entry<K,V>> entrySet;

1.1 负载因子

当哈希冲突严重时,键值对在哈希表中的分布将很不均匀,有些桶中没有元素,而有些桶中有很多元素;此时,查询性能将受到影响。

因此,不能等到HashMap中键值对数量,达到或超过哈希表长度时,才进行扩容。

使用loadFactor(小于1)衡量哈希表的饱和程度。要求size > capacity * load factor时,就进行扩容。通过rehash来减少哈希冲突,以保证HashMap的性能

1.2 分离链表法

嵌套类Node<K,V>,即键值对,是单链表结构。树化时使用嵌套类TreeNode<K,V>

树化只为提高哈希冲突严重时,在桶中查找某个key的效率。 红黑树本质是自平衡的二叉查找树。

image.png
image.png

使用分离链表法的哈希表:

image.png
image.png

2 主要方法实现

2.1 初始化

创建HashMap时,可指定初始容量和loadFactor。如果不指定,则使用默认值。

image.png
image.png
image.png
image.png

也可用一个Map实例来创建新Map。

image.png
image.png
2.1.1 哈希表的尺寸

当指定initialCapacity时,会通过计算得到tableSize。HashMap中哈希表长度,要求始终是2的次方数。便于使用&与运算计算余数。

tableSize-1的二进制表示,除最高位外其余全是1;与key的hashCode做与运算,即可得到对哈希表长度的余数。

image.png
image.png

如,tableSizeFor(12)=16tableSizeFor(32)=32

2.1.2 延迟创建哈希表

在HashMap的构造方法中,并没有立即初始化哈希表,而是在发生第一次put时,才创建哈希表。

image.png
image.png

2.2 put实现

image.png
image.png

先看hashCode计算,HashMap中做了优化。

image.png
image.png

核心逻辑在putVal()中,逻辑如下:

  1. 如果table为null或length为0,则初始化哈希表;
  2. 根据哈希值,使用与运算计算桶下标i;
  3. 如果桶为空,则指直接放入;
  4. 如果桶不为空,则在红黑树或链表中put;
  5. 如果桶中已存在key,则覆盖旧值,返回;
  6. 最后,真的新增了一个entry后,判断是否需要扩容。
image.png
image.png

注意:

  • 在判断key是否已存在时,要求hash值相同,且要求equals()返回true;
  • 当桶中entry是链表时,使用尾插法
  • HashMap中先执行put,后处理扩容;由于使用size > threshold判断,不会导致无效扩容。如容量为16,负载因子为0.75,threshold=12;当插入第13个key时,才会触发扩容;
  • afterNodeInsertion()定义了插入entry后的额外动作,是一个扩展点。
  • 参数onlyIfAbsent为true时,put操作将只插入新key,不覆盖已存在的key(除非旧value为null)。

2.3 resize实现

主要逻辑如下:

  • 当对HashMap第一次put时,将通过resize()来触发哈希表的初始化。
  • 先将threshold设置为触发下次扩容时的键值对总数。如当前哈希表长度为16,threshold=12;扩容后哈希表长度为32,threshold=24;
  • 遍历每一个桶,将其中的键值对重新rehash。

需要注意,resize时对链表也采用尾插法。

image.png
image.png

为什么resize能减少哈希冲突程度呢?

比如,在长度为16的哈希表中,在index=2的桶中,有key=2、18、34、82、98共5个key; 在resize后哈希表长度变为32,2、34、98将仍在index=2的桶中,而18、82会被移动到index=2+16的桶中。 从而降低了哈希桶中哈希冲突

2.4 get实现

get方法调用了getNode(),在key不存在时返回null。

image.png
image.png
image.png
image.png

getOrDefault()在key不存在时,可以返回给定值。

image.png
image.png

2.5 size相关方法

实现很简单。

image.png
image.png
image.png
image.png

2.6 contains相关方法

containsKey时间复杂度是o(1)。

image.png
image.png

containsValue是O(n),与size成正比。

image.png
image.png

2.7 树化和反树化

2.7.1 树化

只有向HashMap真的插入一个键值对时,才可能触发桶的树化。

当某个哈希桶中键值对数量大于等于7时,将尝试对链表树化:binCount即桶中entry的计数

image.png
image.png

但是,当哈希表长度小于64时,不会树化,而是扩容。

image.png
image.png
2.7.2 反树化

反树化实现是java.util.HashMap.TreeNode#untreeify。

image.png
image.png

当哈希桶中键值对数量减少时,都可能触发反树化。如remove、resize等操作。

桶由树退化为链表的条件,是桶中entry数小于等于6时。

image.png
image.png

3 总结

3.1 与JDK7比较

JDK7中,HashMap的桶不会树化,始终是链表结构。在JDK8中,HashMap引入了红黑树

image.png
image.png

扩容差异:

  • 在JDK7中,当HashMap的容量达到阈值时,才会触发扩容。
  • 在JDK8中,除了容量达到阈值外,当哈希表长度小于64时,某个桶中的链表长度大于等于7时,也会触发扩容。(树化只是提高了桶中查询效率,而扩容直接削弱了哈希冲突程度,效果更好

链表的插入方式:

  • 在JDK7中,put、resize操作时,对链表使用头插法,在并发扩容时,可能形成环形数据结构,导致死循环;
image.png
image.png
  • 在JDK8中采用头插法,来避免出现死循环。

3.2 注意事项

  • 在设置HashMap的初始容量时,应考虑map中的键值对预期数及其负载因子,以尽量减少rehash操作的数量。
  • 作为一般规则,默认负载因子(0.75)在时间和空间成本之间提供了一个很好的权衡,最好不要自己指定。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 数据结构
    • 1.1 负载因子
      • 1.2 分离链表法
      • 2 主要方法实现
        • 2.1 初始化
          • 2.1.1 哈希表的尺寸
          • 2.1.2 延迟创建哈希表
        • 2.2 put实现
          • 2.3 resize实现
            • 2.4 get实现
              • 2.5 size相关方法
                • 2.6 contains相关方法
                  • 2.7 树化和反树化
                    • 2.7.1 树化
                    • 2.7.2 反树化
                • 3 总结
                  • 3.1 与JDK7比较
                    • 3.2 注意事项
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档