HashMap第2讲——put方法源码及细节

上篇文章介绍了HashMap在JDK 1.8前后的四大变化,今天就进入到put方法的源码解析。HashMap的设计非常巧妙,细节也很多,今天来看看部分细节,后续的文章会一一介绍。

ps:学习源码的目的不仅仅是为了了解它的运行机制,更重要的是学习它的思想和编码技巧,每一行的源码都可能都经过了“千锤百炼”,才得以呈现在大家眼前。

一、put方法流程图

先上流程图,如下:

二、put方法源码注释

ps:以下代码,JDK版本均为1.8,如有别的版本会有说明。

2.1 几个重要的参数

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
    //默认长度为16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大长度为2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的负载因子为0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //链表-->红黑树条件之一:链表长度大于等于8  
    static final int TREEIFY_THRESHOLD = 8;
    //链表-->红黑树另一个条件:数组长度大于等于64
    static final int MIN_TREEIFY_CAPACITY = 64;
    //红黑树-->链表条件:链表长度小于等于6
    static final int UNTREEIFY_THRESHOLD = 6;
    //存储元素的数组,总是2^n
    transient Node<K,V>[] table;
    //存放具体元素的集合
    transient Set<Map.Entry<K,V>> entrySet;
    //存放元素的个数,注意这个不等于数组的长度
    transient int size;
    //修改次数
    transient int modCount;
    //临界值,当实际大小(容量*负载因子)超过这个值,会进行扩容
    int threshold;
    //加载因子
    final float loadFactor;
}

2.2 put()方法

put方法很简单,就一行代码:

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

核心是putVal方法,在执行putVal方法之前先调用了hash(key)方法获取了一下hashCode。

我们来看下putVal方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //当前hash散列表的引用
    Node<K,V>[] tab;
    //散列表中的元素
    Node<K,V> p;
    //n:数组长度,i:数组索引(寻址的结果)
    int n, i;
​
    if ((tab = table) == null || (n = tab.length) == 0){
        //说明table还没初始化,调用resize进行扩容
        //懒加载:如果在初始化的时候就创建散列表,势必造成空间的浪费
        n = (tab = resize()).length;
    }
​
    if ((p = tab[i = (n - 1) & hash]) == null){
        //说明寻址到的桶的位置没有元素,说明没出现hash冲突
        //那么就直接将key-value封装到Node中并放到下标为i的位置。
        tab[i] = newNode(hash, key, value, null);
    } else {
        //说明该位置有数据了,也就是产生hash冲突了
​
        //看看散列表中的元素的key值是否和插入的key一样
        //一样就赋值,不一样就为null(下面要用)
        Node<K,V> e;
        //临时的key
        K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))){
            //说明当前桶的key值与要插入的key值一样,给e赋值
            e = p;
        } else if (p instanceof TreeNode){
            //说明已经树化了。
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        } else {
            //链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //说明是最后一个元素了
                    //也说明元素在链表中也没有重复,那么就直接添加到链表尾部
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1){
                        //当前元素已经是7了,再来一个就是8
                        //那么就需要进行扩容或者转为红黑树了
                        treeifyBin(tab, hash);
                    }
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))){
                    //说明找到了和插入元素一样的元素了,直接结束循环
                    break;
                }
                p = e;
            }
        }
        if (e != null) {
            //赋值为原来旧值(也就是散列表中的值)
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null){
                //onlyIfAbsent为false
                //替换为新插入的value值
                //ps:putIfAbsent()方法该参数为true
                e.value = value;
            }
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //修改一次散列表结构,那么modCount++
    ++modCount;
    //ps:并发场景下++操作会导致size小于真实个数
    if (++size > threshold){
        //添加后元素个数大于扩容阈值,进行扩容
        resize();
    }
    //啥也没干(空方法)
    afterNodeInsertion(evict);
    //原位置没有值,返回null
    return null;
}

三、hash()方法解读

该方法的功能是根据key的hashCode来定位传入的K-V在数组的索引位置,最简单的办法就是调用Object的hashCode()的方法,然后根据返回值再对数组长度-1进行取模(%)就行。

但是HashMap没有这么做(当然也不会这么做😂),下面我们来看看HashMap 1.7和1.8的实现:

//JDK 1.7
final int hash(object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof string) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }
   
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
//1.7计算索引位置是单独一个方法
static int indexFor(int h,int length){
    return h & (length-1)
}
​
//JDK 1.8
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为了提高hash方法的效率,主要采用了两种手段。

3.1 使用&代替%

先说一下计算索引位置的优化,也就是hash&(length-1)。

我们知道位运算(&)是直接对内存数据进行操作,不需要转为十进制,所以效率高的多。但是,位运算真的能实现取模运算吗?

有这样一个公式——X%2^n = X&(2^n-1)

也就是说,一个数对2的n次幂取模就 == 这个数与2的n次幂-1进行与运算。

假设X=10,n=3,则10%8=2,10&7=2:

记住这个公式就行,大家也可以去多试试加深印象。

ps:所以,这也是为什么HashMap的容量要设为2^n,因为不是2^n的话就不能用位运算来计算索引的位置了。(后续的文章再聊)

除了性能之外,还有一个好处就是可以很好的解决负数的问题:我们知道hashCode的结果是int类型,而它的取值范围是-2^31~2^31-1,这里面是包含负数的,如果用取模处理负数是很麻烦的,而如果用位运算,length-1一定是的正数,所以它的第一位一定是0,这就保证了h&(length-1)的结果一定是个正数。

3.2 扰动计算

经过3.1的介绍,现在我们的公式就变为key.hashCode() & (length-1),显然HashMap也不是这样做的,取的是将key的hashCode右移+异或运算(^)的结果。

那么为啥要这样做,如果直接用key.hashCode的呢?我们举个例子:

假设数组长度为8,如上图,那么结果只取决于hash值的低三位,无论高位如何变化,结果都是一样的,所以产生hash冲突的几率就比较大。

而如果我们把高位参与运算,则索引的计算结果就不会仅取决于低位,如下图:

可以看到的到的结果就不一样了,所以不论是JDK 1.7还是JDK1.8的扰动计算,目的都是为了让高位参与运算,尽量减少hash冲突。

四、如何解决hash冲突

hash冲突是不可避免的,那么通常怎么解决呢?这里简单介绍5种常用的方法,感兴趣的可以去深入了解一下:

  • 开放定址法(ThreadLocalMap):一旦发生冲突,就去找下一个为空的散列地址,直到找到位置。

  • 链地址法(HashMap):每个哈希桶指向一个链表,发生冲突时,新的元素会挂到链表末尾或放到红黑树相应的位置。

  • 再哈希:当发生冲突时,使用其它函数计算另一个哈希函数地址,直到没冲突。

  • 建立公共溢出区:将哈希表分为基本表和移除表两部分,发生冲突的元素都放在溢出表中。

  • 一致性哈希:通过将数据均匀分布到多个节点来减少冲突。

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/683792.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

idea的代码没有提交到仓库怎么撤回到本地?

代码已经提交到变更列表但是还没有push推送到仓库上&#xff0c;可以用这个方法 点击日志-右键要撤回的记录-选择撤销提交 撤销的又回到本地变更 当然你只能撤销自己提交的&#xff0c;别人的你撤销不了

AI基础设施是AI落地赋能的核心关键

AI基础设施内涵与特性 以深度落地赋能为导向&#xff0c;AI供给侧持续推进技术要素全面融合、技术能力自主可控、技术服务普惠低成本&#xff0c;AI供给“基建 化”势在必行&#xff0c;AI基础设施正成为AI的关键供给形态。算法、算力、数据是AI技术应用的三大核心支撑要素&am…

SpringBoot+Vue在线视频课程网站(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 角色对应功能 用户教师管理员 系统功能截图

Gradio.NET:一个快速制作演示demo网页的利器

Gradio介绍 Gradio是一个用于创建机器学习模型交互界面的Python库。它允许开发者快速为他们的模型创建一个简单的web界面&#xff0c;以便于非技术用户和其他开发者进行交互和测试。 Gradio的主要优点是易用性和灵活性。你只需要几行代码就可以为你的模型创建一个交互界面。你…

【Python数据挖掘实战案例】机器学习LightGBM算法原理、特点、应用---基于鸢尾花iris数据集分类实战

一、引言 1、简要介绍数据挖掘的重要性和应用 在数字化时代&#xff0c;数据已经成为企业和社会决策的重要依据。数据挖掘作为一门交叉学科&#xff0c;结合了统计学、机器学习、数据库技术和可视化等多个领域的知识&#xff0c;旨在从海量数据中提取有价值的信息&#xff0c…

Marvelous Designer中一些棉质布料预设

Marvelous Designer中一些棉质布料预设的解释&#xff1a; Cotton_14_Wale_Corduroy&#xff1a;14条细鲸鱼纹的灯芯绒&#xff0c;适合制作温暖且有质感的服装。Cotton_40s_Chambray&#xff1a;40支精梳针织的府绸布&#xff0c;通常用于制作休闲衬衫。Cotton_40s_Poplin&am…

echars饼图、柱状图 java返回的数据格式

1、echars饼状图返回的数据格式 [ { "name": "A", "value": 3 }, { "name": "B", "value": 2 }, { "name": "C", "value": 2 } ] java代码Demo 为例&#xff1a;根据名字分组&…

vuInhub靶场实战系列--prime:1

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 nmap主机扫描2.1.3 arp-scan主机扫描 2.2 端口扫描…

【CMake系列】06-项目结构与输出路径管理

为了对大型项目实现更好的管理【模块化协作开发等等】&#xff0c;cmake 提供了很多指令&#xff0c;可以对项目的结构进行调整、管理&#xff0c;便于项目的合理规划。本文我们要学习的就是 项目结构的设置&#xff0c;以及 构建程序等 输出路径的设置 本专栏的实践代码全部放…

倾斜侧壁增强光提取效率相关机制的建模仿真研究

较低的光提取效率&#xff08;LEE&#xff09;是制约深紫外发光二极管&#xff08;LED&#xff09;快速发展的一个重要因素&#xff0c;倾斜侧壁结构可以直接将横向传播的横向磁场&#xff08;TM&#xff09;偏振光散射到c面逃逸锥&#xff0c;从而提高器件的LEE&#xff0c;因…

review of c++

友元关系是单向的。 指针

什么是数字化转型?

作者&#xff1a; 峡山老曹 数字神化 ”企业如何实现数字化转型“是摆在现代企业面前一个无法回避的问题&#xff0c;数字化转型的重要性不容忽视&#xff0c;它不仅是企业适应数字化时代的必然要求&#xff0c;更是提升竞争力、实现可持续发展的关键。随着科技的飞速发展和市场…

MFTCoder论文被KDD 2024接收,开源v0.4.2版发布

1. MFTCoder 简介 CodeFuse在2023年9月开源了一种多任务微调框架——MFTCoder&#xff0c;它可以实现在多个任务上同时并行地进行微调。通过结合多种损失函数&#xff0c;我们有效地解决了多任务学习中常见的任务间数据量不平衡、难易不一和收敛速度不一致等挑战。大量实验结果…

【C语言】文件操作(下卷)

前言 在上一卷中&#xff0c;我们知道了文件指针、文件的打开和关闭&#xff08;打开其他位置的文件&#xff09;、文件的顺序读写&#xff08;其中的fputc()、fgetc()&#xff09;&#xff0c;这一卷中&#xff0c;将继续讲解文件操作未讲到的地方。 内容有点多&#xff0c;…

Vue3【三】 使用TS自己编写APP组件

Vue3【三】 使用TS自己编写APP组件 运行截图 目录结构 注意目录层级 文件源码 APP.vue <template><div class"app"><h1>你好世界!</h1></div> </template><script lang"ts"> export default {name:App //组…

redsystems教程的基本使用之重置密码(忘记密码解决方法)

前言&#xff1a; 相信很多人都有疑惑&#xff0c;要是我不记得密码怎么办&#xff1f;如果你登录了&#xff0c;点击更改密码后&#xff0c;还是要你填写登录密码才能修改。为了解决这问题&#xff0c;博主通过了钻研成功搞出来了&#xff01;&#xff01;&#xff01;&#…

重学java 64.IO流 字符流

Action speak louder than words —— 24.6.5 字符输入流 一、字节流读取中文的问题 1.注意&#xff1a; 字节流是万能流&#xff0c;这个万能更侧重于文件复制&#xff0c;但是尽量不要边读边看 2.原因&#xff1a; UTF-8&#xff1a;一个汉字占三个字节 GBK&#xff1a;一…

SQLAlchemy 模型中数据的错误表示

1. 问题背景 在使用 SQLAlchemy 0.6.0 版本&#xff08;也曾尝试使用 0.6.4 版本&#xff09;的 Pylons 应用程序中遇到了一个 SQLAlchemy ORM 问题。该问题出现在使用 psycopg2 作为数据库驱动程序、连接至 Postgresql 8.2 数据库的环境中。定义了一个 User 模型对象&#xf…

专为成长型企业打造的Java CRM系统源码:CRM客户关系管理系统技术解析与功能构建

一、序章 在激烈的市场竞争环境中&#xff0c;客户关系管理&#xff08;CRM&#xff09;系统是企业保持竞争优势、提高客户满意度、推动业务发展的核心工具。本文将深入探讨一款集成了现代技术的CRM系统&#xff0c;该系统基于Spring Cloud Alibaba与Spring Boot构建&#xff…

关于安装typescript后运行tsc -v命令报错问题

报错信息&#xff1a; tsc 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 没有配置环境变量&#xff0c;使用npm命令查看typescript的安装目录&#xff1a; npm config get prefix 根据控制台输出的目录&#xff0c;配置path环境变量 tsc -v 运行成功&…