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

news2024/10/6 2:22:00

上篇文章介绍了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:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java——Stream流(2/2):Stream流的中间方法、终结方法(方法、案例演示)

目录 Stream流的中间方法 方法 案例演示 Stream流的终结方法 方法 案例演示1 收集 案例演示2 Stream流的中间方法 方法 中间方法指的是调用完成后会返回新的Stream流&#xff0c;可以继续使用(支持链式编程)。 Stream提供的常用中间方法说明Stream<T> filter(P…

Ansible部署 之 zookeeper集群

简介 Ansible是近年来越来越火的一款轻量级运维自动化工具&#xff0c;主要功能为帮助运维实现运维工作的自动化、降低手动操作的失误、提升运维工作效率。常用于自动化部署软件、自动化配置、自动化管理&#xff0c;支持playbook编排。配置简单&#xff0c;无需安装客户端&am…

【小白专用 已验证24.5.30】ThinkPHP6 视图

ThinkPHP6 视图 模板引擎支持普通标签和XML标签方式两种标签定义&#xff0c;分别用于不同的目的 标签类型描述普通标签主要用于输出变量、函数过滤和做一些基本的运算操作XML标签也称为标签库标签&#xff0c;主要完成一些逻辑判断、控制和循环输出&#xff0c;并且可扩展 c…

keil下载及安装(社区版本)

知不足而奋进 望远山而前行 目录 文章目录 前言 Keil有官方版本和社区版本&#xff0c;此文章为社区版本安装&#xff0c;仅供参考。 1.keil MDK 2.keil社区版介绍 3.keil下载 (1)打开进入登录界面 (2)点击下载,跳转到信息页面 (3)填写个人信息,点击提交 (4)点击下载…

基于web的垃圾分类回收系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;用户管理&#xff0c;公告管理&#xff0c;运输管理&#xff0c;基础数据管理 用户账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;运输管理&#xff0c;公告…

Web安全:Web体系架构存在的安全问题和解决方案

「作者简介」&#xff1a;2022年北京冬奥会网络安全中国代表队&#xff0c;CSDN Top100&#xff0c;就职奇安信多年&#xff0c;以实战工作为基础对安全知识体系进行总结与归纳&#xff0c;著作适用于快速入门的 《网络安全自学教程》&#xff0c;内容涵盖系统安全、信息收集等…

Python算法于强化学习库之rlax使用详解

概要 在强化学习领域,开发和测试各种算法需要使用高效的工具和库。rlax 是 Google 开发的一个专注于强化学习的库,旨在提供一组用于构建和测试强化学习算法的基础构件。rlax 基于 JAX,利用 JAX 的自动微分和加速计算功能,使得强化学习算法的实现更加高效和简洁。本文将详细…

植物大战僵尸杂交版2.0.88最新版安装包

游戏简介 游戏中独特的杂交植物更是为游戏增添了不少亮点。这些杂交植物不仅外观独特&#xff0c;而且拥有更强大的能力&#xff0c;能够帮助玩家更好地应对游戏中的挑战。玩家可以通过一定的条件和方式&#xff0c;解锁并培养这些杂交植物&#xff0c;从而不断提升自己的战斗…

SpringBoot:SpringBoot中使用Redisson实现分布式锁

一、前言 Redisson是一个在Redis的基础上实现的Java驻内存数据网格&#xff08;In-Memory Data Grid&#xff09;。它不仅提供了一系列的分布式的Java常用对象&#xff0c;还提供了许多分布式服务。 刚好项目中需要使用到分布式锁&#xff0c;记录一下Redisson是如何使用分布式…

SpringBoot+Vue幼儿园管理系统(前后端分离)

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

C++ STL - 容器

C STL&#xff08;标准模板库&#xff09;中的容器是一组通用的、可复用的数据结构&#xff0c;用于存储和管理不同类型的数据。 目录 零. 简介&#xff1a; 一 . vector&#xff08;动态数组&#xff09; 二. list&#xff08;双向链表&#xff09; 三. deque&#xff08…

k8s Pods漂移时间配置

默认为300秒 apiVersion: apps/v1 kind: Deployment metadata:name: my-test spec:replicas: 1selector:matchLabels:app: my-apptemplate:metadata:labels:app: my-appspec:containers:- name: my-containerimage: nginx:latestports:- containerPort: 80tolerations:- key: &…

LabVIEW传感器虚拟综合实验系统

LabVIEW传感器虚拟综合实验系统 开发了一个基于LabVIEW的传感器虚拟综合实验系统&#xff0c;该系统集成了NIELVIS和CSY系列传感器实验平台&#xff0c;通过图形化编程语言进行数据处理和实验管理。系统允许用户进行多种传感器参数的测量和实验报告的自动生成&#xff0c;支持…

使用DLL还是通讯协议进行LabVIEW设备集成

在使用LabVIEW进行设备集成时&#xff0c;可以选择通过设备提供的DLL或直接使用通讯协议。选择方法取决于开发效率、性能、灵活性和维护成本等因素。本文将从这几个方面详细分析两种方法的优劣&#xff0c;帮助做出最佳决策。 分析角度 1. 开发效率&#xff1a; DLL&a…

全能大模型AIGC产品的使用体验、选择倾向与未来展望

随着人工智能技术的迅猛发展&#xff0c;AIGC&#xff08;人工智能生成内容&#xff09;领域正迎来前所未有的繁荣。其中&#xff0c;全能大模型产品以其强大的生成能力和广泛的应用场景&#xff0c;吸引了众多用户的关注。腾讯元宝APP的上线更是为这一领域注入了新的活力。本文…

Echarts柱状图数据太多,自定义长度之后,自适应浏览器缩放

不知道是不是最优解&#xff0c;但是当前解决了我遇到的问题&#xff0c;如有更好的方法&#xff0c;希望看到这篇文章的同学可以不吝指导一番&#xff0c;非常感谢 1、问题描述&#xff1a; 因Ecahrts柱状图数据有时多有时少&#xff0c;所以在数据达到一定程度之后&#xff…

spring mvc 中怎样定位到请求调用的controller

前言 在java web开发过程中&#xff0c;正常情况下controller都是我们自己写的&#xff0c;我们可以很方便的定位到controller的位置。但是有些时候我们引入的其他依赖中可能也有controller&#xff0c;为了找到并方便的调试jar包中的controller&#xff0c;我们一般会进行全局…

【CMake系列】10-cmake测试集成googletest与第三方库自动化构建

cmake测试&#xff0c;使用ctest 可能不能满足我们的需求&#xff0c;需要我们使用更为强大的第三方测试框架&#xff0c;如googletest&#xff0c;完成项目中的测试工作 本篇文章将第三方测试框架 googletest&#xff0c;引入&#xff0c;同时也可以作为 关于 第三方包 自动化…

Nginx编译安装,信号,升级nginx

编译安装nginx&#xff1a;前面博客有写编译安装过程 systemctl stop firewalld setenforce 0 mkdir /data cd /data wget http://nginx.org/download/nginx-1.18.0.tar.gz tar xf nginx-1.18.0.tar.gz cd nginx-1.18.0/ yum -y install make gcc pcre-devel openssl-devel …

0基础学习区块链技术——51%攻击和双花

在《0基础学习区块链技术——分叉》一文中&#xff0c;讲解了区块链分叉相关的原因。这种分叉是CAP原则的结果&#xff0c;大部分分布式系统都会优先保证Availability&#xff08;可用性&#xff09;和Partition Tolerance&#xff08;分区容错性&#xff09;。而对于Consisten…