多线程编程(12)之HashMap1.8源码分析

        之前已经分析过了一版1.7版本的HashMap,这里主要是来分析一下1.8HashMap源码。

一、HashMap数据结构

        HashMap 是一个利用散列表(哈希表)原理来存储元素的集合,是根据Key value而直接进行访问的数 据结构。

  •  JDK1.7 中,HashMap 是由 数组+链表构成的。

  •  JDK1.8 中,HashMap 是由 数组+链表+红黑树构成

 数组: 优势:数组是连续的内存,查询快(o1 )劣势:插入删除O(N) 链表: 优势:不是连续的内存,随 便插入(前、中间、尾部)插入O(1) 劣势:查询慢O(N)。

二、HashMap源码深度解析

2.1 成员变量与内部类

	// 默认数组容量,16,左移4位既16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 最大容量,左移30位,即2的30次幂static final int MAXIMUM_CAPACITY = 1 << 30;// 负载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链表转红黑树阈值static final int TREEIFY_THRESHOLD = 8;// 当链表的值小于6红黑树转链表static final int UNTREEIFY_THRESHOLD = 6;// 做红黑树转换的hashmap容量大小,如果前面单个链表个数大于8,但是hashmap容量小于64则直接扩容不做红黑树转换static final int MIN_TREEIFY_CAPACITY = 64;// hashmap得数组,中间状态数据transient Node<K,V>[] table;// 用来存放缓存、中间状态数据transient Set<Map.Entry<K,V>> entrySet;// hashmap的实时数据transient int size;// 用来记录hashmap中K-V的修改次数transient int modCount;// 扩容临界值int threshold;// 负载因子final float loadFactor;// 具体存放数据的地方,JDK 1.7之前存放的是Entry,JDK1.8之后存放的是node节点static class Node<K,V> implements Map.Entry<K,V> {// hash值final int hash;// key值final K key;V value;// 下一个节点指针Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}

2.2 HashMap构造器

    public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}

         使用默认构造函数时,在put之前和之后分别debug以上变量信息对比看看。

        第一次put之后:

  接下来我们使用自定义初始化参数验证:

         在有参数构造时,最终tableSizeFor。

    /*** 带参数的初始化其实threshold就是调用这个函数* 其实这里最主要的作用就是将cap转成n的指数倍* 首先将n转成2进制,右移再和自己取或,相当于把里面所有的0变成1* 最终的目的,找到>=n的,1开头后面全是0的数,例如n整数为17,那么二进制为10001 -1 =10000,经过不断右移或自己,那么高位都会变成了11111* 最后n的值就为:11111+1=100000,对应的整数值就是32*/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;}

        总结:

  • 无参数构造时,容量为16,因子=0.75,第一次插入数据时,才会初始化table阈值等信息。
  • 有参构造函数,会取大于但是最接近你容量的2的指数倍。
  • 无论哪种构造方式,扩容阈值最终都是=容量*因子。

2.3 HashMap插入方法

        1)先了解以下流程图。

2)关于key做hash值的计算

        当我们调用put方法添加元素的时候,实际是调用了其内部的putVal方法,第一个参数需要对key求hash值。

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

         然后我们来看下hash是怎么取值的。

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

图解:

结论:使用移位异或运算做第二次扰动,不是直接使用hashcode。

3)核心逻辑:

	/*** onlyIfAbsent:true不更改现有值* evict:fakse表示table为创建状态*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// 临时变量,tab=数组,p=插槽的指针,n=tab的长度,i为数组下标Node<K,V>[] tab; Node<K,V> p; int n, i;// 数组是否为null或者size长度=0,第一次put的时候初始化if ((tab = table) == null || (n = tab.length) == 0)// 初始化数组or扩容n = (tab = resize()).length;// 寻址,后续会将具体源码if ((p = tab[i = (n - 1) & hash]) == null)// 获取得到的坐标为空,则直接新的node放在插槽上tab[i] = newNode(hash, key, value, null);else {/*** 如果存在有值那么说明存在hash碰撞了,需要追加成链表了* e:是否找到与当前key相同的节点,找到说明是更新,null说明是新key插入* k:临时变量,查找过程中的key在这里暂存*/Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) // 如果第一个正好是相同的e = p; // 将p赋值给e,要注意此时还没有覆盖,只是单单的标记到了e,标记找到相同key的节点。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);// 一直遍历到最后判断找不到存在一样的key,那么直接插入到末尾,并且这里会判断是否需要转换成红黑树,内部会判断时候数据大小是否大于64,否则先做扩容if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break; // 遍历的过程如果找到一样的,那么赋值给ep = e;}}if (e != null) { //如果e是非空的,那么说明前面循环中找到了一个跟当前key相同的值V oldValue = e.value;// 判断是否需要覆盖if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}// 用来标记修改次数++modCount;// 判断当前大小是否超过阈值,如果超过则做扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

4)寻址计算

        接着上面说的  i = (n - 1) & hash],这个寻址计算,我们来看下下面的例子:

其实在上面已经说到过了,我们的hash值是去hashcode然后跟本身右移16做异或运算得到最终扰动之后的hash值,然后这里跟对应的数组长度做与运算,hashcode不会超长了吗,我们来看下面的例子:

         其实可以看得到,其实就是拿hashcode的对应数组长度的低位做运算,hashcode超出那部分就不要了。

        就是不管你算出来的hash是多少,超出tab长度的高位会被抹掉,低位是多少就是你所在的槽的位置,也就是对应table的下标。

        这里可能有人会问了,为什么不做取模运算,取模也会保证不会超出来数组长度,其实这里做位运算的效率比取模运算是要高非常多的!!!!

2.4 hashmap扩容方法

        看下图:

 

        核心源码resize方法:

	 /*** 这个方法包含了初试化以及扩容的方法*/final Node<K,V>[] resize() {// 原先的数组Node<K,V>[] oldTab = table;// 原数组长度,如果没有初始化那么就是0int 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)// 原数组长度左移一位,其实就是*2newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // HashMap(int initialCapacity, float loadFactor)初始化的时候调用newCap = oldThr;else {               // HashMap() 初始化的时候调用,注意前面验证过了,是在第一次put的时候调的newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) { // 如果新阈值位空,那么则根据负载因子重新计算阈值float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 以上各种操作就是各种容量相关值的计算,下面是计算之后重新迁移数据@SuppressWarnings({"rawtypes","unchecked"})// 按照新容量来创建新的数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;// 如果原数据不为空,则开始迁移数据if (oldTab != null) {for (int j = 0; j < oldCap; ++j) { // 遍历数组Node<K,V> e; // 临时变量,记录的是当前指向的node节点if ((e = oldTab[j]) != null) {oldTab[j] = null; // 方便gcif (e.next == null) // 不存在下一个节点,既只有一个节点,那么直接复制到新数组的索引下即可newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode) // 如果是树节点,拆成两拼到新table上((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { /*** 如果是链表那么则拆成两个链表* loHead:低位链表* hiHead:高位链表* 在上面我们说过了,其实我们是拿数组长度对应的hashcode的低位做与运算来得到对应的数组下标* 现在数据扩容了两倍,就是就是多拿了一位hashcode对应高位来做运算,如果运算结果为0,那么代表hashcode的高一位为0,那么这node节点迁移到新数组上则是在低位* 如果返回的是1,那么代表高位的是1,则迁移到新数组上面去这个节点应该是在高位* 其实这也是为什么hashmap是2倍扩容的原因。*/Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead; // 原数据下标}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead; // 高位,原数据下标+原数组长度}}}}}return newTab;}

总结:

  • 扩容就是将旧表的数据迁移到新表。
  • 迁移过去的值需要重新计算hashcode,也就是他存储位置
  • 如果计算位置,采用低位链表和高位链表,如果位置下面的e.hash&oldCap等于0,那么它对应的就是低位链表,也就是数据位置保持不变。
  • e.hash & old不等于0就是要重写计算他的位置,也就是j+oldCap,就是高位链表位置。 例如原数组长度为16,16的二进制为:10000,原先计算hash是拿hashcode & (16-1)就是参与运算的二进制就是,1111,然后现在拿16来做与运算,就是判断原先的key的高一位是否是0,还是1,如果是1,那么与运算返回的结果就不是0,那么这个位置就是应该放在高位链表,否则表示这个key的原先的hashcode的高一位数为0,然后与运算返回的结果就是0,则应该放在低位链表。

2.5 HashMap获取方法

        

       

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Text Control 控件 中 Service Pack 3:MailMerge 支持 SVG 图像

图像的合并方式与报告模板中的合并字段相同。占位符在设计时添加&#xff0c;并与文件、数据库或内存中的数据合并。可以将图像对象添加到具有指定名称的模板中。数据列必须包含字节数组形式的二进制图像数据、System.Drawing.Image 类型的对象、文件名、十六进制或 Base64 编码…

产品经理-需求收集(二)

1. 什么是需求 指在一定的时期中&#xff0c;一定场景中&#xff0c;无论是心理上还是生理上的&#xff0c;用户有着某种“需要”&#xff0c;这种“需要”用户自己不一定知道的&#xff0c;有了这种“需要”后用户就有做某件事情的动机并促使达到其某种目的&#xff0c;这也就…

Python 开心消消乐

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

记一次绕过宝塔防火墙的BC站渗透

0x00 信息收集 由于主站存在云waf 一测就封 且初步测试不存在能用得上的洞 所以转战分站 希望能通过分站获得有价值的信息 这是一个查询代理帐号的站 url输入admin 自动跳转至后台 看这个参数 猜测可能是thinkCMF 0x01 getshell thinkcmf正好有一个RCE 可以尝试一下 ?afetc…

01.爬虫---初识网络爬虫

01.初识网络爬虫 1.什么是网络爬虫2.网络爬虫的类型3.网络爬虫的工作原理4.网络爬虫的应用场景5.网络爬虫的挑战与应对策略6.爬虫的合法性总结 1.什么是网络爬虫 网络爬虫&#xff0c;亦称网络蜘蛛或网络机器人&#xff0c;是一种能够自动地、系统地浏览和收集互联网上信息的程…

SpringValidation

一、概述&#xff1a; ​ JSR 303中提出了Bean Validation&#xff0c;表示JavaBean的校验&#xff0c;Hibernate Validation是其具体实现&#xff0c;并对其进行了一些扩展&#xff0c;添加了一些实用的自定义校验注解。 ​ Spring中集成了这些内容&#xff0c;你可以在Spri…

一文教你如何调用Ascend C算子

Ascend C是CANN针对算子开发场景推出的编程语言&#xff0c;原生支持C和C标准规范&#xff0c;兼具开发效率和运行性能。基于Ascend C编写的算子程序&#xff0c;通过编译器编译和运行时调度&#xff0c;运行在昇腾AI处理器上。使用Ascend C&#xff0c;开发者可以基于昇腾AI硬…

AC/DC电源模块:提供高质量的电力转换解决方案

BOSHIDA AC/DC电源模块&#xff1a;提供高质量的电力转换解决方案 AC/DC电源模块是一种电力转换器件&#xff0c;可以将交流电转换为直流电。它通常用于各种电子设备和系统中&#xff0c;提供高质量的电力转换解决方案。 AC/DC电源模块具有许多优点。首先&#xff0c;它能够提…

【学习笔记】计算机组成原理(七)

指令系统 文章目录 指令系统7.1 机器指令7.1.1 指令的一般格式7.1.2 指令字长 7.2 操作数类型和操作类型7.2.1 操作数类型7.2.2 数据在存储器中的存放方式7.2.3 操作类型 7.3 寻址方式7.3.1 指令寻址7.3.1.1 顺序寻址7.3.1.2 跳跃寻址 7.3.2 数据寻址7.3.2.1 立即寻址7.3.2.2 直…

探秘SpringBoot默认线程池:了解其运行原理与工作方式(@Async和ThreadPoolTaskExecutor)

文章目录 文章导图Spring封装的几种线程池SpringBoot默认线程池TaskExecutionAutoConfiguration&#xff08;SpringBoot 2.1后&#xff09;主要作用优势使用场景如果没有它 2.1版本以后如何查看参数方式一&#xff1a;通过Async注解--采用ThreadPoolTaskExecutordetermineAsync…

Samtec技术漫谈 | 电动自行车中的传感器和信号传输技术

【摘要/前言】 电动自行车&#xff0c;大家熟悉吗&#xff1f; 今天的话题似乎是可以唤起大家心底骑车的美好回忆&#xff0c;我们也曾骑车探索过大自然和社区&#xff0c;自行车也是我们曾经不可或缺的便捷交通工具。 怀旧思潮的影响&#xff0c;加持科技的进步&#xff0c…

Flask 蓝图路由的模块化开发

基于 Flask 蓝图路由的模块化开发 1. 编程目标 为了提高Flask应用的可维护性和可扩展性&#xff0c;我们通过使用Flask的蓝图(Blueprint)功能&#xff0c;可以将不同的功能模块拆分到独立的文件中&#xff0c;方便后续的开发和维护。 2. 项目结构 项目结构树如下&#xff1…

Uni-App开发 导入(引入)Vant-Weapp组件;支持vue3/vue2版本和微信小程序

文章目录 目录 文章目录 操作流程 小结 概要安装流程技术细节小结 概要 Vant Weapp官网&#xff1a;Vant Weapp - 轻量、可靠的小程序 UI 组件库 准备工作&#xff0c;需要确保自己的电脑上已安装Hbuilde和node 全程操作的环境都需要这些配合才能运行上&#xff0c;可参考作者…

M功能-支付平台(三)

target&#xff1a;离开柬埔寨倒计时-221day 前言 今天周六&#xff0c;但是在柬埔寨还是工作日&#xff0c;想着国内的朋友开始休周末就羡慕呀&#xff0c;记不清在这边过了多少个周六了&#xff0c;多到我已经习惯了。而且今天技术部还停电了&#xff0c;真的是热的受不了呀…

【MATLAB源码-第74期】基于matlab的OFDM-IM索引调制系统不同频偏误码率对比,对比OFDM系统。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 OFDM-IM索引调制技术是一种新型的无线通信技术&#xff0c;它将正交频分复用&#xff08;OFDM&#xff09;和索引调制&#xff08;IM&#xff09;相结合&#xff0c;以提高频谱效率和系统容量。OFDM-IM索引调制技术的基本思想…

Android 性能为王时代SparseArray和HashMap一争高下

文章目录 一、SparseArray 源码分析1. **类定义和构造函数**2. **基本方法**2.1 put(int key, E value)2.2 get(int key)2.3 delete(int key)2.4 removeAt(int index)2.5 gc()2.6 size()2.7 keyAt(int index) 和 valueAt(int index) 3. **辅助方法**3.1 binarySearch() 二、使用…

【SpringCloud】负载均衡

目录 负载均衡什么是负载均衡生活场景为什么需要负载均衡负载均衡手段负载均衡总的来说有两种实现手段负载均衡具体可以通过多种手段来实现 SpringCloud中的负载均衡组件Ribbon VS Nginx负载均衡区别集中式LB进程内LB RibbonRibbon的工作原理Ribbon在工作时分成两步 使用1.提供…

昔日辉煌不再,PHP老矣,尚能饭否?

导语 | 近期 TIOBE 最新指数显示&#xff0c;PHP 的流行度降至了历史最低&#xff0c;排在第 17 名&#xff0c;同时&#xff0c;在年度 Stack Overflow 开发者调查报告中&#xff0c;PHP 在开发者中的受欢迎程度已经从之前的约 30% 萎缩至现在的 18%。“PHP 是世界上最好的语言…

【开源】大学生竞赛管理系统 JAVA+Vue+SpringBoot+MySQL

目录 一、系统介绍 学生管理模块 教师管理模块 竞赛信息模块 竞赛报名模块 二、系统截图 三、核心代码 一、系统介绍 基于Vue.js和SpringBoot的大学生竞赛管理系统&#xff0c;分为管理后台和用户网页端&#xff0c;可以给管理员、学生和教师角色使用&#xff0c;包括学…

【Flutter】Dialog组件PageView组件

&#x1f525; 本文由 程序喵正在路上 原创&#xff0c;CSDN首发&#xff01; &#x1f496; 系列专栏&#xff1a;Flutter学习 &#x1f320; 首发时间&#xff1a;2024年5月27日 &#x1f98b; 欢迎关注&#x1f5b1;点赞&#x1f44d;收藏&#x1f31f;留言&#x1f43e; 目…