多线程编程(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.mzph.cn/pingmian/16831.shtml

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

相关文章

内网穿透端口映射内外网反弹 shell 回顾

内网穿透&端口映射&内外网反弹 shell 回顾 内网概念 我们常说的内网&#xff0c;一般指的是非公有 IPv4 地址的网络比如学校机房里的电脑一般为私有网络&#xff0c;家里的网络也是私有网络 私有网络的范围与划分 ipaddressareaA 类地址10.0.0.0&#xff5e;10.255.…

521源码-免费下载-WordPress全能自动采集与发布插件 – WP-AutoPostPro 汉化版

更多网站源码学习教程&#xff0c;请点击&#x1f449;-521源码-&#x1f448;获取最新资源 本工具下载地址&#xff1a;WordPress全能自动采集与发布插件 – WP-AutoPostPro 汉化版 - 521源码 WP-AutoPostPro是一款出类拔萃的WordPress自动采集发布插件&#xff0c;凭借其卓…

Yolov5保姆及入门-含源码【推荐】

前言 YOLO系列模型作为一种实时目标检测算法&#xff0c;自从YOLO1发布以来&#xff0c;就以其检测速度快、准确率高而受到广泛关注。随着技术的迭代&#xff0c;YOLO系列已经发展到了YOLO8。本文将详细介绍YOLO5的技术规格、应用场景、特点以及性能对比。 yolov5源码下载地址…

海外真实机房给云手机上“福利”

不论是做出海跨境方面的业务&#xff0c;大家都不自觉的把目光放在了海外的云手机上&#xff0c;尤其是有直播群控&#xff0c;引流获客这样的一个刚需&#xff0c;只有处在海外真实环境了&#xff0c;那么在一些活动的过程中&#xff0c;才能表现的更稳&#xff0c;而不会触发…

Android --- 交换两个布局

准备布局 exchange_out_layout exchange_in_layout <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"andr…

免费,Python蓝桥杯等级考试真题--第13级(含答案解析和代码)

Python蓝桥杯等级考试真题–第13级 一、 选择题 答案&#xff1a;C 解析&#xff1a;正向下标由0开始&#xff0c;下标3代表第四个元素&#xff0c;故答案为C。 答案&#xff1a;A 解析&#xff1a;range&#xff08;0,4&#xff09;的取前不取后&#xff0c;元组的符号是小括…

【CGAL】Region_Growing 检测平面并保存

目录 说明一、算法原理二、代码展示三、结果展示 说明 本篇博客主要介绍CGAL库中使用Region_Growing算法检测平面的算法原理、代码以及最后展示结果。其中&#xff0c;代码部分在CGAL官方库中提供了例子。我在其中做了一些修改&#xff0c;使其可以读取PLY类型的点云文件&…

【搭建大语言模型】使用LocalGPT搭建本地大语言模型服务并实现远程访问进行交互

文章目录 前言环境准备1. localGPT部署2. 启动和使用3. 安装cpolar 内网穿透4. 创建公网地址5. 公网地址访问6. 固定公网地址 前言 本文主要介绍如何本地部署LocalGPT并实现远程访问&#xff0c;由于localGPT只能通过本地局域网IP地址端口号的形式访问&#xff0c;实现远程访问…

设计模式15——享元模式

写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用&#xff0c;主要是下面的UML图可以起到大作用&#xff0c;在你学习过一遍以后可能会遗忘&#xff0c;忘记了不要紧&#xff0c;只要看一眼UML图就能想起来了。同时也请大家多多指教。 享元模式&#xff08;Flyweigh…

Linux信号:信号的概念及意义

目录 一、什么是信号 kill-l查看信号 二、信号的产生 2.1系统调用 kill raise abort 2.2软件条件 13)SIGPIPE pipe信号 14&#xff09;SIGAKARM alarm信号 2.2硬件中断 2.3异常 8)SIGFPE 除0异常 11)SIGSEGV 野指针 2.4信号处理的常见方式 三、Core Dump和term…

一文详解SaaS增长模式:PLG、MLG、SLG哪种更适合你?

在SaaS&#xff08;软件即服务&#xff09;的领域中&#xff0c;增长策略的选择对于企业的成功至关重要。其中&#xff0c;PLG&#xff08;产品驱动增长&#xff09;、MLG&#xff08;市场驱动增长&#xff09;和SLG&#xff08;销售驱动增长&#xff09;是三种常见的策略&…

Centos 7 安装刻录至服务器

前言 在日常测试中&#xff0c;会遇到很多安装的场景&#xff0c;今天给大家讲一下centos 7 的安装&#xff0c;希望对大家有所帮助。 一.下载镜像 地址如下&#xff1a; centos官方镜像下载地址https://www.centos.org/download/ 按照需求依次点击下载 二.镜像刻录 镜像刻…

强悍!轻量级 Viedo Download 项目!!【送源码】

今天给大家分享一个非常轻量实用的命令行视频下载项目&#xff1a;Lux。 项目简介 Lux是一款基于Golang编写的快速、简单的视频下载库和命令行工具&#xff0c;支持众多个流行的视频网站&#xff0c;包括 YouTube、Bilibili、优酷、爱奇艺、腾讯视频、抖音快手等。 界面简洁易…

重生之 SpringBoot3 入门保姆级学习(07、整合 Redis 案例)

重生之 SpringBoot3 入门保姆级学习&#xff08;07、整合 Redis 案例&#xff09; 导入 Maven 依赖并刷新 Maven <dependencies><!--springboot3 Web 依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring…

高效至臻,Media Encoder 2024 for Mac——您的专业媒体处理首选

Media Encoder 2024 for Mac是一款专为Mac用户打造的专业视频和音频编码工具&#xff0c;凭借其卓越的编码能力和智能编辑功能&#xff0c;为用户提供了前所未有的媒体处理体验。无论是高清、4K还是8K的视频文件&#xff0c;Media Encoder 2024都能轻松驾驭&#xff0c;确保在压…

学习javascript的函数

1.什么是函数&#xff1f; 可以重复被使用的代码块 作用&#xff1a;函数可以把具有相同或者相似逻辑的代码“包裹起来”&#xff0c;有利于代码的复用。 2.函数的基本使用 1.定义函数 利用关键字Function 定义函数&#xff08;声明函数&#xff09; function 函数名(){函…

4、xss-labs之level4、lecel5

一、level4 1、测试分析 level4跟之前的3一样的思路&#xff0c;闭合value的值&#xff0c;但是通过双引号闭合&#xff0c;然后使用onclick的属性弹窗绕过 输入3的payload&#xff1a; 2、所以构造payload payload&#xff1a;"οnclickalert(1)// 二、level5 1、测…

uniapp高校二手书交易商城回收系统 微信小程序python+java+node.js+php

每年因为有大量的学生在接受教育&#xff0c;每到大学毕业季的时候&#xff0c;所使用的大量书籍对他们自己来说&#xff0c;很多是没有用&#xff0c;同时由于书籍多和不方便携带&#xff0c;导致很多大学生在毕业时将教材直接丢弃是在校大学生处理已用教材的一种主要方式。然…

数据结构-二叉树系统性学习(四万字精讲拿捏)

前言 这里我会从二叉树的概念开始讲解&#xff0c;其次涉及到概念结构&#xff0c;以及堆的实现和堆排序。 目的是&#xff0c;堆比二叉树简单&#xff0c;同时堆本质上是二叉树的其中一种情况&#xff0c;堆属于二叉树顺序结构的实现 最后完善二叉树的讲解&#xff0c;也就是…

Java与Gradle 的版本兼容性矩阵验证

1.下面这个表格显示了java和gradle的版本兼容性情况 2.根据上面这份表格理解&#xff0c;是不是java17就需要gradle 7.3之后来支持。用android studio 来试验一下: jdk选择: build成功: 说明JDK17并不是一定需要Gradle 7.3之后版本 3.使用JDK1.8、JDK11验证一下Grade 7.2是否可…