通俗易懂的HashMap源码分析

易语言-HashMap模块源码—— 高效随机存取数据结构 ,文本索引必备 1、此HashMap类采用java jdk中HashMap的实现方式 2、相比网站上发布过的hashtable之类的源码: 此HashMap寻址方法是拉链法.比开放寻址法对连续内存要求更低 哈希函数用的是java中String.hashCode()算法(经实际验证其碰撞率极低且相近的文本散列值相邻,存取的效率更高.) 可自动无限增加容量(内存允许) 3、连续10万次不同内容存取效率为,存10万次共耗时约280ms ,取10万次共耗时约330ms 。 (注意:请编译后测试效率,由于大量内存操作,编译后效率约为调试时的100倍以上。) fangzhiguo 立即下载

据说给我点关注的都成了大佬,点关注的我都会私发一份好东西

前言

HashMap是我们最常用、最常见的一种数据结构。使用起来也非常的方便,主要是通过键值对的方式进行存储,简单明了。
本文将基于Java1.8版本的HashMap源码进行分析,适合Java、Android等初学者以及想了解源码的中高级工程师阅读交流。

① 数据结构
1.1 图示

在这里插入图片描述

上图即可简单示意HashMap的整体数据结构,主要是通过数组+链表(满足一定条件时,链表会切换成树)
数组定义为Node类型的数组:

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;
        
        // 以下省略若干代码
}    

Node对象里第一个变量为key进行hash后的值,具体的hash过程为:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

第二个变量为key,第三个变量为value,最后一个变量为指向链表的下一个对象(如果有的话)。

1.2 为什么这么设计

举个栗子:
我们都有手机吧,可以参考手机的联系人。
我们可以把人的名字看作key,名字的首字母认为是调用hash()函数的返回值,对应的手机号就是我们想要查找到的value。

现在,我们如果想查找“张三”的手机号,我们会怎么做?

我们当然是先确认张三的首字母 “Z”(类似于HashMap对key进行hash的过程),然后快速的滑到“Z”开头的联系人,找到“Z”之后,如果“Z”中只有一个刚好是张三,那就直接查找到了,如果有很多个,就需要再按顺序查找“张三”(类似于通过hash值找到数组中的一个链表,再通过key值找到这个链表中的“张三”这个key)。

所以这样设计存储和取值都比较快速。
当我们拿到一个key,就可以快速的定位到数组中位置,因为数组的下标是通过key计算得来的,更准确的来说是通过key的hash后的值计算得到的。

当确定一个key以后,我们通过hash()计算得出一个hash值,然后按照下面的方法将hash值取模在0~size-1范围内,作为数组的下标:

hash & (size -1) // 相当于取模

这样取模比较高效,但是size必须为2的次幂(转换成二进制就是1后面跟n个0,减一后,就是1变成0,n个0变成n个1)。

那怎么保证size是2的次幂呢,这里先卖个关子。
所以通过key计算出数组的下标,我们就能够快速的定位到数组的位置进行取值,当然不同的key,hash之后通过取模后可能得到相同的值,这就是hash冲突,为了预防冲突,我们对key进行了二次hash(参考上文中hash函数),如果有冲突,那我们将使用链表将不同Key,哈希取模相同的进行存储,这样我们取值的时候需要遍历下链表。这个过程和我们查找手机联系人很相似。

② put的流程
2.1 为什么容量一定是2的次幂

上文中我们提到怎么保证size一定是2的次幂,假设我们使用的时候完全可以传一个非2的次幂大小呢。看源码怎么干的:

public HashMap(int initialCapacity, float loadFactor) {
        // 此处省略无关代码
        this.threshold = tableSizeFor(initialCapacity);
}

成员变量threshold本来是用来存储用来判断是否要进行扩容的一个阈值(容量 * 负载因子),但是在HashMap数组还没有创建的时候,这个变量会暂时用来存储初始化的容量值,所以当我们通过类似new HashMap(15)这种方式实例化HashMap时,实际容量的值是需要通过tableSizeFor(initialCapacity)方法处理后返回。那么我们传进去的15会被怎么处理返回呢?

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法就一个目的,将我们传入的数字一顿操作给整成2的次幂。所以,我们在new HashMap的时候,无论我们怎么传,最后创建数组的时候大小都是2的次幂。到这里,我们很好的解释了为什么可以通过**hash & (size -1)**进行取模了。

2.2 put是如何实现的

其实上面多多少少讲到了怎么取数据,那么往里存储数据其实也差不多,也就是先根据key计算Hash值然后取模找到数组的下标,然后看当前下标的数组中是否有值,有的话,得再判断下这里面是否有和当前要存储的key相同的,相同的话就将值进行覆盖,如果没有相同的话,就新增一个放到链表的头的位置。
这里涉及到一个问题,就是HashMap中的数组是什么时候创建的?并不是在我们使用new HashMap的时候就创建哦,是在我们第一次put的时候,接下来我们稍微摘选看下源码:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}

这个方法调了另一个方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            
        // 省略其他代码    
}

可以看到当我们第一次put数据的时候,table是空的,那么就会调用resize()方法创建个数组,resize的主要职责是进行扩容的,下面会详细介绍,第一次调用的时候就是创建个数组。
数组创建完了就是将需要存入的key-value存入了:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            
}            

就是将key、value、以及key的hash()后的值封装到Node里面,然后存储到数组里。这是第一次put的情况,如果不是第一次存储,也就是我上面描述的逻辑,无非就是替换还是新增。这里要强调一点,当一个hash坐标上单链表的长度超过一定的值,这个单链表会被切换成树来存储。

static final int TREEIFY_THRESHOLD = 8;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    //省略不相关代码 
     
	if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    	treeifyBin(tab, hash);
    
    // 省略	
}    	

就是单链表的长度达到8就会调用treeifyBin函数来进行切换数据结构。这个函数我们来简单研究一下:

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

第一个判断条件是判断当前数组的大小是否达到需要调整为树结构的最小容量(MIN_TREEIFY_CAPACITY = 64),如果没有,优先进行扩容操作,而不是进行数据结构调整。
如果达到了,下面的代码也比较容易懂了,先通过hash值定位到数组的位置,并将这个值赋值给e,接着通过replacementTreeNode函数转换成TreeNode<K,V>对象,并将hd指向第一个树节点,将tl也指向第一个树节点,然后判断单链表的结点e是否有下一个,如果有还是取出来赋值给e,并将其转换成树节点赋值给p,这个时候因为之前tl已经赋值过了,所以不为null,所以会将p.pre指向tl,tl的next指向p,这样一轮循环就将单项链表变成了双向链表,再调用**treeify()**函数将双向链表变成红黑树。这个函数就不再详细展开了。

③ 扩容机制
3.1 重要参数

讲到扩容机制就不得先讲讲一些重要参数了:

// 默认的数组容量,1左移4位,相当于2的4次幂=16,当我们实例化HashMap不传容量大小时,默认为此值
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  

// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;  

// 默认的负载因子,负载因子✖️容量 就等于需要扩容的临界值了
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 存储的键值对数量
transient int size;

3.2 扩容实现

相信大家都了解过HashMap有个扩容的机制,当我们往HashMap中存储键值对时,如果达到了一定的数值(容量✖️负载因子),就被认为我们当前开辟的数组的容量即将不够用了,所以提前申请更大的数组容量,以便后续继续put更多的数据。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        
        // 省略...
        
       Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  
       
       if (oldTab != null) {
       	// 省略 ...将旧数组的数据转移到新的数组上
       }
       
       return newTab;
}

当oldCap > 0时,才有扩容的事,所以我们看下这个if语句中的逻辑,如果旧的数组容量已经是最大容量了,这个时候我们也无法扩容了,返回原来的数组。
如果将旧的数组容量扩容一倍后还是小于最大容量的,并且旧数组的容量是大于或者等于默认容量的,就将旧的容量的阈值也扩大一倍。
然后将按照新的数组容量创建一个新的数组。
然后将旧的数组上的数据转移到新的数组上。最后返回新的数组及完成扩容。

④ 补充:fail-fast
4.1 简介

按照字面的意思翻译为:快速失败机制,他是Java集合中一种错误检测机制。操作集合的时候我们常常会去迭代循环操作,当在迭代操作的时候我们改变了数据结构,就有可能抛出ConcurrentModificationException异常,这个机制就是一个错误检测机制,为了防止我们不当的对集合操作带来不可预期的数据错误,所以,抛出这个异常用来提醒我们防止数据错误未及时发现而导致更严重的影响。但是,这个JDK也并不能保证100%能帮我们检测出来,借用源码中的一句话是**Fail-fast iterators

  • throw ConcurrentModificationException on a best-effort basis**。
4.2 如何实现
// 当HashMap里面数据的数量被改变或者数据结构发生改变的操作,都会被记录到这个变量上
transient int modCount;

这个变量就是作为判断抛出fail-fast的重要依据,当我们调用remove()、clear()等函数时会进行加一操作,那我们看看什么情况下会发生fail-fast呢?

if (map.modCount != expectedModCount)
	throw new ConcurrentModificationException();
	
if (modCount != mc)
	throw new ConcurrentModificationException();

主要就是上面两种类型的,第一种类型,对于第一种,expectedModCount在迭代器被实例的时候将当前的modCount赋值,如果使用迭代器的过程中modCount和实例化时候的值不一样就会抛出这个异常。
第二种类型也是类似的,mc的值也是将expectedModCount对其进行赋值的。

好的,到这里我们已经基本掌握HashMap的实现了,当然我们是忽略了一些实现细节的,看源码我们主要看下实现思想就行,太纠结细节是没有必要的(当然除了一些关键细节)。

推荐阅读:
漫画演示:HTTPS原理还能这么讲?太有趣了吧!

易语言-易语言高性能哈希表模块和例程 最近做项目需要用到哈希表,由于易语言没有原生的哈希表 在网上倒是有一些网友自己写的哈希表 但是都不是很满意 像“闪电哈希”,“scripting.Dictionary”对象 伪哈希 不是速度太慢 就是功能不全 本哈希类为模仿java中 jdk哈希表 只是处理冲突键时,一直使用的线性表 立即下载

相关推荐

哈希表类_汇编版(HashMap_ASM) 2.7版-易语言

2.7版(2020.4.7)     1) 调整 添加() 方法. 增加可选参数 参_不覆盖 (当键值存在时不进行值覆盖)     2) 添加 添加_指针() 方法. (具体用法参考使用例子)     3) 添加 cha询_指针() 方法. (具体用法参考使用例子)     4) 添加 有序模式,创建时可选参数.(此模式下  取所有键() 取所有值()  将按添加时的顺序来取出数组,时间复杂度: O(1).具体用法参考使用例子)     5) 添加 有序_插入() 方法. (有序模式下使用.)     6) 添加 有序_取序号() 方法. (有序模式下使用.)     7) 添加 有序_取序号键值() 方法. (有序模式下使用.通过遍历序号取键值,性能较低,单次取值时间复杂度: O(n),非必要不建议使用)     8) 说明 枚举键值还是乱序枚举,因为即便是有序模式xx的储存依然是无序的.可使用 取所有键() 取所有值() 存到数组来遍历. 2.6版(2020.3.31)     1) 修复 值运算() 方法 异常问题. (感谢 精易论坛  【qq1347522182】 的反馈)     2) 添加 寻找文本键 方法(功能类似 是否包含文本键 方法,多2个参数【参_开始位置】【参_返回键名长度】).     3) 调整 创建() 方法 添加参数 参_不使用内存池 (默认为假(使用内存池), 为真则不使用内存池(xx量不大时可以选择不使用内存池以节省内存使用))     4) 优化 自定义xx_模版 类 (优化后无需对自定义xx成员进行引用,只要将模版内的【自定义xx】类型 全部更改成 所需的类型即可) 2.5版(2020.1.11)     1) 修复 模块初始化优先级问题导致的异常. 2.4版(2019.12.16)     1) 优化 内部添加内存池,使用私有堆申请内存时改为内存池来分配内存,添加() 和 载入表() 性能得到显著提升。 2.3版(2019.12.9)     1) 修复 取值的键数组 逻辑值获取失败问题。     2) 调整 长整数键的哈希计算调整为单独的方法调用,减少些汇编代码。

HashMap类

胡杨(Java学生) 330

易语言仿java集合 list map源码

易语言面相对象编程仿java集合 list, 以及map工具类 模块ec源码

map 容器的详细总结

map 容器的详细总结。

zxy_ZXY123的博客 1272

易语言-易语言利用自定义数据类型和数组. 制作键对值操作类/内存配置

易语言利用自定义数据类型和数组.  制作键对值操作类/内存配置 @kn剑齿虎

易语言取配置项节名模块源码

资源介绍:。取配置文件下某节的配置项名称。易语言取配置项节名模块源码例程程序寻找文本实现取配置项节名功能。易语言源码例程属于易语言初级教程。资源作者:。自学编程网。资源界面:。资源下载:。

最通俗易懂的 HashMap 源码分析解读

最通俗易懂的 HashMap 源码分析解读

qq_41490913的博客 135

易语言HashMap类

易语言HashMap类源码,HashMap类,初始设置,加入,取值,删除,清空,取所有键,取所有值,枚举所有键,键总数,是否为空,是否存在键,取所有键值对,计算散列值,更新阈值,计算索引,重新索引

Java HashMap 深入浅出 源码分析,通俗易懂

HashMap 在数据结构中我门学习过很多种查找的方式 比如顺序查找,折半查找(二分查找),分块查找以及此topic中提到的散列查找。 散列查找也为哈希查找,哈希查找的时间复杂度为O(1),插入的时间复杂度也为O(1),有着比较好的性能,但是始终存在一个难以解决的问题就是哈希冲突。 为了解决这个冲突,衍生出很多的解决办法 拉链法(将有哈希冲突的字符串成一个链表) 开放地址法 线性探测法 平方探测法 为随机序列法 以上方法的本质思想都相同,就是如果遇到了冲突就查找currentPostion+di 每种

weixin_42293662的博客 227

51单片机基础知识

前言 周末闲着无聊,以后定时更新一期关于51单片机的学习案例,以供大家平时参考使用。至于单片机,你可以理解为人的大脑,而人各项身体器官的运作需要意识给到大脑,然后大脑再驱动我们的中枢神经进行相应的动作,这里的意识可以理解为我们要写的程序,而人的器官相当于各种输出设备,例如我们以后要学的LED、电机、各种传感器等等。学好单片机能让你找到一份相对不错的工作,当然越可观的东西也是要付出相应努力的。 一、51单片机是什么? 通常我们一说到 51 单片机,指的都是兼容IntelMCS-51体系架构的一系列单片机,而

qq_43537819的博客 6903

点餐系统数据库设计--SQL Server

友情链接 1、学生成绩管理系统数据库设计–MySQL 学生成绩管理系统数据库设计–MySQL 2、医疗信息管理系统数据库–MySQL 医疗信息管理系统数据库–MySQL 3、邮件管理数据库设计–MySQL 邮件管理数据库设计–MySQL 4、SQL Server医疗信息管理系统数据库【英文版-源码】–(Medical Management System Database) SQL Server医疗信息管理系统数据库【英文版-源码】–(Medical Management System Database) 5

若如初见 8万+

HashMap源码分析(通俗易懂)(JDK17)

在 Java 中,HashMap是一个非常常用的Map接口的实现类,它基于哈希表(数组 + 链表/红黑树)实现键值对存储,提供O(1)时间复杂度的查找、插入、删除操作。本文将基于 JDK 17 源码,详细分析HashMap的内部结构、关键方法以及扩容机制。HashMap是基于哈希表的实现,采用数组和链表(或红黑树)结合的方式存储键值对。它通过哈希值计算槽位索引,在哈希冲突时采用链表或红黑树解决。当链表长度超过阈值时会转为红黑树,提高查找性能。通过put()get()resize()等方法,

yuan723999518的博客 947

WIFI中CSI的读取

学习目标: 目前正在做基于WIFI的无设备定位,准备使用信道状态信息(CSI),学习一下。 目前采集数据环境已经搭建好,准备研究信号的预处理。 学习内容: 1、 信号的读取——read_bf_file函数的研究 2、 3、 4、 1. read_bf_file详解: %READ_BF_FILE Reads in a file of beamforming feedback logs. % This version uses the *C* version of read_bfee, ...

sz3076799的博客 8891

HashMap源码分析

文章目录简介继承关系存储结构源码分析属性Node节点TreeNodeHashMap 构造方法put 添加方法待更新 简介 在我们对数据存储的时候都会有数据结构这种东西,但是传统的底层数据结构比如数组或链表,不够好用,所有一些大神就为我们对这些基础数据结构作了进一步的封装,有了更高级的数据结构,而HahsMap就是由此而来,HashMap采用key/value存储结构,每个key对应唯一的value。查询和修改的速度都很快,能达到O(1)的平均时间复杂度。它是非线程安全的,且不保证元素存储的顺序 继承关系

念念不忘,必有回响 5698

YOLOv8改进——引入可变形卷积DCNv3

本文只讲解在YOLOv8的代码中添加DCNv3的操作流程, 具体的原理参见上述的链接~是pytorch实现的版本,只要基础的pytorch环境安装正确就不会出错。是C++实现版本,必须先在上一步编译成功,或者安装好了轮子,否则会报错。但是在实际的训练过程中,C++版本的运行速度更快,推荐使用C++版本。(具体怎么修改,都可以自己决定,然后通过实验看看效果如何)文件夹,该文件夹下的内容就是实现DCNv3算子的核心代码。中,下图中的列表里添加。

zyw2002的博客 3万+

J-Flash手动添加华大HC32F460型号全攻略

在嵌入式开发中,Flash编程算法是连接调试工具与微控制器的重要桥梁,它定义了芯片存储器的擦除、编程和校验等底层操作规范。其原理是通过调试接口将算法代码加载到目标芯片的RAM中执行,从而实现对Flash存储器的访问与控制。这项技术的核心价值在于打破了工具链对芯片型号的固有支持限制,使得开发者能够灵活适配各种新兴或定制化的微控制器。在工程实践中,当面对J-Link等主流调试器官方支持包更新滞后时,手动添加芯片型号成为保障开发效率的关键技能。本文以华大半导体的HC32F460系列MCU为例,详细解析了如何通过配

weixin_29192365的博客 349

HashMap 源码分析

loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。默认的初始化大小为 16。

Wzideng@qq.com 1445

【立创EDA-PCB设计基础】6.布线铺铜实战及细节详解

本文进行布线铺铜实战及详解布线铺铜的细节

嵌入式阿齐Archie 4万+

HashMap扩容机制源码分析

前几天写了一篇,ArrayList扩容源码分析。好像源码也没有我们想象的那么可怕?(当然了,只是简单的分析,后面等我知识充足了,将进一步的分析) 今天本来想打游戏的,但是网速太差了,真是的是让人火爆。努力写代码,将来租一个网速好的房子 1、具体看源码之前,我们先简单的说一下HashMap的底层数据结构     1、HashMap底层的数据结构是 数组 ...

小道仙的后宫 1584

电磁阀颤振工作原理

颤振 颤振,弹性结构在均匀气(或液)流中受到空气(或液体)动力、弹性力和惯性力(见达朗伯原理)的耦合作用而发生的大幅度振动,它是气动弹性力学中最重要的问题之一。 随着机电液一体化技术的发展,电液比例控制技术的应用越来越广泛。电液比例控制系统范围较广,在液压传动与控制过程中,系统能够接受模拟信号或数字信号,使输出的流量或压力连续成比例地变化,都属于此类。该控制系统的优点是充分利用了液压控制与电气控制的长处,即功率传递靠液压,信号处理靠电气的优化措施。 电液比例控制原理与要求 液压控制系统中两个最重要的被控参数

小幽余生不加糖 1万+

完整功能-《网络安全创新实践》:设计并实现一种基于Npcap的网络嗅探器(直接能运行cpp的MFC代码+实验报告)

《网络安全创新实践》:设计并实现一种基于Npcap的网络嗅探器。(直接能跑的MFC)监听所有数据包,以及监听指定IP地址的数据包;(2)能够显示完整的IP数据包首部信息,例如:版本、首部长度、数据包长度、数据包类型(协议字段)、TTL、源IP地址、目的IP地址等;(3)能够显示完整的TCP报文段信息,例如:源端口、目的端口、序号、确认号、数据部分等;(4)能够自动检测SQL注入攻击等Web攻击行为;(5)能够将监听到的数据包信息保存到文件中,以及从文件中读取并显示数据包信息;(6)具有图形化操作界面。

weixin_46531010的博客 660
上一篇: Android dp转px不要再用这么low的方式写了!
下一篇: Leakcanary原理解析以及换肤框架skin的原理分析
IT烂笔头
博客等级 码龄15年 621粉丝 78原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值