【底层学习】HashMap源码学习

成员变量

// 默认初始容量 就是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;// 默认加载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;// 树化阈值(链表转为树的阈值)
static final int TREEIFY_THRESHOLD = 8;// 取消树化阈值(红黑树转为链表)
static final int UNTREEIFY_THRESHOLD = 6;// 链表转红黑树的数组长度的临界值 
static final int MIN_TREEIFY_CAPACITY = 64;// hashmap中的数组结构
transient Node<K,V>[] table;// HashMap中的元素个数
transient int size;// HashMap被修改的次数
transient int modCount;// 要调整大小的下一个大小值,计算公式是(容量 * 加载因子)。
int threshold;// 实际的加载因子
final float loadFactor;

注意:
JDK1.7 及以前:HashMap 底层是数组+链表
JDK1.8 之后:HashMap 底层是数组+链表 或者 数组+红黑树
存储在 HashMap 集合中的元素都将是一个 Map.Entry 的内部接口的实现
什么时候采用数组+链表,什么时候采用数组+红黑树,以及相互转换,取决于链表(红黑树)的长度。当链表长度大于 8 会转为红黑树,当链表长度小于 6,红黑树转为链表。

构造函数

注意:以下部分是基于JDK1.8进行编写的

空参构造

// 使用默认初始容量 (16) 和默认负载系数 (0.75) 构造一个空的 HashMap 。
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他所有字段都是默认
}

有一个参数的构造函数

构造一个空的 HashMap ,具有指定的初始容量和默认负载系数 (0.75)。

public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

有两个参数的构造函数

构造一个具有指定初始容量和负载因子的空 HashMap

public HashMap(int initialCapacity, float loadFactor) {// 初始化容量小于0,错误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;//调用tableSizeFor方法计算出不小于initialCapacity的最小的2的幂的结果,并赋给成员变量thresholdthis.threshold = tableSizeFor(initialCapacity);
}

tableSizeFor 方法计算源码如下:

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;
}

有一个Map类型参数的构造方法

public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;// 调用PutMapEntries()来完成HashMap的初始化赋值过程putMapEntries(m, false);
}

putMapEntries 方法调用了HashMap的resize()扩容方法和putVal()存入数据方法

HashMap 中的方法

put方法

流程图:
在这里插入图片描述
思路:

  1. 第一次 put 会先进行扩容
  2. 找到要插入的位置
    2.1. 这个位置没值:直接插入
    2.2. 这个位置有值:
    2.2.1. 判断是否 key 存在,存在的话覆盖 value
    2.2.2. 不存在,判断是否红黑树
    2.2.2.1. 如果是红黑树:调用树化插值方法
    2.2.2.2. 如果是链表:加到链表尾部,判断转树阈值,大于 8 转为红黑树
  3. 判断长度是否满足扩容(长度*扩容因子)?若满足,则动态扩容

在这里插入图片描述

源码如下:代码大部分都标有注释,有些代码需要反复阅读可能会理解的更好

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;// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 找到具体的数组下标,如果这个位置没有值的话,就直接在这个位置初始化node放在这里就好了if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 走到这里说明这个位置有值Node<K,V> e; K k;// 如果插入的key一样,并且hash值也相等,如果是,取出这个节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果该节点是代表红黑树的节点,调用红黑树的插值方法else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {// 走到这,说明数组该位置上是一个链表for (int binCount = 0; ; ++binCount) {// 插入到链表的最后面(Java7 是插入到链表的最前面)if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个// 会触发下面的 treeifyBin,也就是将链表转换为红黑树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;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;// 判断是否需要扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

resize方法

思想:用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移。

流程图:
在这里插入图片描述

get方法

在这里插入图片描述

源码如下:

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 判断第一个节点是不是就是需要的if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {// 判断是否是红黑树if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 链表遍历查找do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}// 找不到返回nullreturn null;
}

常问面试题

HashMap的key可以为 null 嘛?

可以,具体源码如下:

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

当 key 为 null 的时候,hash 值为 0,所以这也是为什么 hashmap 只允许一个 key 为 null

hash 方法的实现及为什么这样实现?

参考上面源码,这样做原因是为了减少高位没有参与下标的计算从而引起的碰撞。

为什么要用异或运算符?

保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞

拉链法导致链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

二叉查找树在特殊情况下会变成一条线性结构(也就是单腿树,这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。红黑树的话,会进行左旋,右旋,变色等操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题

HashMap 存取数据的时间复杂度

不管插入还是查找,由 key 获取 hash 定位到桶的时间复杂度都是 O(1)。
如果桶里面没有元素,那么时间复杂度就是 O(1)。
若有元素,是链表,就要遍历查询,时间复杂度 O(n),如果是红黑树,那么时间复杂度就是 O(logn)。

参考链接:https://blog.csdn.net/LoveMyTail/article/details/107286727

HashMap寻址算法

计算对象的hashCode(),在调用hash()方法进行二次哈希,hashCode值右移16位再异或运算,让哈希分布更为均匀,最后(capacity-1)&hash计算得到索引

为什么HashMap数组长度一定是2的次幂?

计算索引时效率更高:如果是2的n次幂可以使用位与运算代替取模运算
扩容时重新计算索引效率更高:hash & oldCap == 0的元素留在原位置,否则新位置 = 旧位置 + oldCap

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

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

相关文章

IT廉连看——C语言——结构体

IT廉连看——C语言——结构体 一、结构体的声明 1.1 结构的基础知识 结构是一些值的集合&#xff0c;这些值称为成员变量。结构的每个成员可以是不同类型的变量。 1.2 结构的声明 struct tag {member-list; }variable-list; 例如描述一个学生&#xff1a;typedef struct Stu…

SQL Server添加用户登录

我们可以模拟一下让这个数据库可以给其它人使用 1、在计算机中添加一个新用户TeacherWang 2、在Sql Server中添加该计算机用户的登录权限 exec sp_grantlogin LAPTOP-61GDB2Q7\TeacherWang -- 之后这个计算机用户也可以登录数据库了 3、添加数据库的登录用户和密码&#xff0…

进程与线程之线程

首先exec函数族是进程中的常用函数&#xff0c;可以利用另外的进程空间执行不同的程序&#xff0c;在之前的fork创建子进程中会完全复制代码数据段等&#xff0c;而exec函数族则可以实现子进程实现不同的代码 int execl(const char *path, const char *arg, ... …

Qt之字符串查找

去掉空格 OString类的成员雨数 trimmed&#xff08;&#xff09;会去掉字符申首尾的空格&#xff0c;而成员函数simplified&#xff08;&#xff09;不仅 去掉宇符申首尾的空格&#xff0c;中间连续的空格也用一个空格符来替换。比如&#xff1a; OString str1"Are you O…

C++拿几道题练练手吧

第 1 题 【 问答题 】 • 最短路径问题 平面上有n个点(n<100)&#xff0c;每个点的坐标均在-10000~10000之间。其中的一些点之间有连线。 若有连线&#xff0c;则表示可从一个点到达另一个点&#xff0c;即两点间有通路&#xff0c;通路的距离为两点间的直线距离。现在的任务…

远超 IVF_FLAT、HNSW,ScaNN 索引算法赢在哪?

Faiss 实现的 ScaNN&#xff0c;又名 FastScan&#xff0c;它使用更小的 PQ 编码和相应的指令集&#xff0c;可以更为友好地访问 CPU 寄存器&#xff0c;展示出优秀的索引性能。 Milvus 从 2.3 版本开始&#xff0c;在 Knowhere 中支持了 ScaNN 算法&#xff0c;在各项 benchma…

JavaAPI常用类03

目录 java.lang.Math Math类 代码 运行 Random类 代码 运行 Date类/Calendar类/ SimpleDateFormat类 Date类 代码 运行 Calendar类 代码 运行 SimpleDateFormat类 代码一 运行 常用的转换符 代码二 运行 java.math BigInteger 代码 运行 BigDecimal …

VS项目忽略文件.gitignore模板

Visual Studio项目忽略文件.gitignore模板 ## 忽略Visual Studio临时文件、生成结果和 ## 由流行的Visual Studio加载项生成的文件。 ## ## 从获取最新消息 https://github.com/github/gitignore/blob/main/VisualStudio.gitignore# 用户特定文件 *.rsuser *.suo *.user *.user…

数字孪生的技术开发平台

数字孪生的开发平台可以基于各种软件和硬件工具来实现&#xff0c;这些平台提供了丰富的功能和工具&#xff0c;帮助开发人员构建、部署和管理数字孪生系统&#xff0c;根据具体的需求和技术要求&#xff0c;开发人员可以选择合适的平台进行开发工作。以下列举了一些常见的数字…

一文讲透 map.computeIfAbsent putIfAbsent computeIfPresent区别

map.computeIfAbsent public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)如果 key 对应的 value 不存在&#xff0c;则使用获取 mappingFunction 计算后的值&#xff0c;并保存为该 key 的 value&#xff0c;否则返回 value。 impor…

将python两个版本添加环境变量(Mac版)

在运行程序的时候&#xff0c;可能不知道选择哪个版本的程序来执行&#xff0c;先添加环境变量&#xff0c;然后进行选择。 1、查看python安装路径 which python which python3 来查看各个版本的安装位置 2、编辑环境变量配置文件 Macos使用默认终端的shell是bash&#xff0c…

c入门第二十三篇: 学生成绩管理系统优化(支持远程操作)

前言 师弟高兴的说道&#xff1a;“师兄&#xff0c;你猜我今天上课看见谁了&#xff1f;” 我&#xff1a;“谁呢&#xff1f;” 师弟&#xff1a;“程夏&#xff0c;没想到&#xff0c;她竟然来旁听我们计算机系的课程了。虽然我从前门进去的&#xff0c;但是我还是一眼就看…

swing jdk版本导致的显示尺寸不一致问题

Java Swing JFrame size different after upgrade to JRE11 from JRE 7 or 8. How can I make the frame size consistent? - Stack Overflow 从 JRE 7 或 8 升级到 JRE11 后&#xff0c;Java Swing JFrame 大小不同。如何使帧大小一致&#xff1f; - IT工具网 设置虚拟机选项…

01背包问题:组合问题

01背包问题&#xff1a;组合问题 题目 思路 将nums数组分成left和right两组&#xff0c;分别表示相加和相减的两部分&#xff0c;则&#xff1a; left - right targetleft right sum 进而得到left为确定数如下&#xff0c;且left必须为整数&#xff0c;小数表示组合不存在&…

SSM框架,MyBatis框架的学习(上)

MyBatis简介 MyBatis 是一款优秀的持久层框架&#xff0c;它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO&#xff08;Plain Old Ja…

28. 找出字符串中第一个匹配项的下标(力扣LeetCode)

文章目录 28. 找出字符串中第一个匹配项的下标题目描述暴力KMP算法 28. 找出字符串中第一个匹配项的下标 题目描述 给你两个字符串 haystack 和 needle &#xff0c;请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标&#xff08;下标从 0 开始&#xff09;。…

mapbox高德地图与相机

mapbox高德地图与相机 本案例使用Mapbox GL JavaScript库创建高德地图。 演示效果引入 CDN 链接地图显示 创建地图实例定义地图数据源配置地图图层 设置地图样式实现代码 1. 演示效果 2. 引入 CDN 链接 <script src"https://api.mapbox.com/mapbox-gl-js/v2.12.0/mapb…

Android虚拟机Dalvik和ART

前言&#xff1a;Android虚拟机包括Dalvik和ART&#xff0c;它们是用于在Android设备上运行应用程序的关键组件。 Dalvik虚拟机&#xff1a; 1. 设计目的&#xff1a; Dalvik虚拟机是在Android早期版本中使用的虚拟机&#xff0c;其设计目的是为了在资源受限的移动设备上执…

项目实战:Qt监测操作系统cpu温度v1.1.0(支持windows、linux、国产麒麟系统)

若该文为原创文章&#xff0c;转载请注明出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/136277231 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结…

全面升级!Apache HugeGraph 1.2.0版本发布

图数据库以独特的数据管理和分析能力&#xff0c;在企业数智化转型的过程中正在成为数据治理的核心&#xff0c;根据IDC调研显示&#xff0c;95%的企业认为图数据库是重要的数据管理工具&#xff0c;超过65%的厂商认为在业务上图数据库优于其他选择&#xff0c;尤其是在金融风控…