HashMap 随记

HashMap 构造器


HashMap 共有四个构造器:

public HashMap(int initialCapacity, float loadFactor) {// 对于传入的初始容量(loadFactor) 及 负载因子(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);
}
// 调用了 HashMap(int initialCapacity, float loadFactor),其中 loadFactor 取默认值 DEFAULT_LOAD_FACTOR
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
// 容量及负载因子皆使用默认值
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
// 调用 putMapEntries 将值存入当前 hashmap
public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);
}

在上面构造器中存在一个方法【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;
}

换句话说,HashMap 的默认容量为16,而容量是以2的次方扩充的(即使是自定义传入,也一定会经过转换,如传入30,则返回32),一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算;

HashMap 的实现原理

数据存储方式示例图:
在这里插入图片描述

在 JDK1.8 中,HashMap采用【位桶(数组table)+链表+红黑树】实现,当链表长度超过阈值(8)时,将链表转换为红黑树(table长度大于等于64),这样大大减少了查找时间;链表长度大于8时转化为红黑树,小于6时转化为链表;

HashMap put 方法:

/*** Associates the specified value with the specified key in this map.* If the map previously contained a mapping for the key, the old* value is replaced.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with <tt>key</tt>, or*         <tt>null</tt> if there was no mapping for <tt>key</tt>.*         (A <tt>null</tt> return can also indicate that the map*         previously associated <tt>null</tt> with <tt>key</tt>.)*/
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

其中 hash 方法:

// 计算 hash code
static final int hash(Object key) {int h;// key.hashCode 的调用方法:Object 中的原生方法 public native int hashCode();return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

重头戏,putVal 方法:

/*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;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) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);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;
}

头大,分解来看:

总体结构:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;/*** tab 用于保存 table 引用* 1、若 tab 为 null 或者 tab 的长度为 0,则调用 resize 方法进行初始化或者扩容*/if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;/*** 2、到了这一步,tab 一定存在* i = (n - 1) & hash 确定元素存放在 tab 中的下标,p = tab[i] */// 2.1、若 p 为 null,表示当前 tab 的 i 位置空,则可以直接直接构建 Node 插入if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 2.2、若 p 不为 null,表示 tab 的 i 位置已经有值,则继续进行内部判断Node<K,V> e; K k;// ... 后续单独理解}/*** modCount 自增,记录操作行为次数* 3、++size > threshold,即判断下一次增加一个结点后size是否大于最大容量,如果是,则进行一次扩容*/++modCount;if (++size > threshold)resize();// 插入成功时会调用的方法(默认实现为空)afterNodeInsertion(evict);return null;
}

从 1 ~ 3 共三个步骤中,理解 putVal 方法大的执行方向。其中最复杂的是 2.2 中 else 中 的内容:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// ...else {Node<K,V> e; K k;// 已知:p = tab[i],是 tab[i] 中链表或者红黑树第一个结点// 如果 p.hash 和 p.key 与传入参数中的 hash 和 key 相同,表示对应 key 已经存在,则直接使用原结点,只需要后面改变value即可if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果 p 是红黑树结点类型,则将其插入 tab[i] 位置中的红黑树中else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else { // p 是链表结点类型,则将其插入 tab[i] 位置中的链表中// 循环,尾插法for (int binCount = 0; ; ++binCount) {// 链表尾部if ((e = p.next) == null) {// 构建新结点,并修改 p 的 next 指向p.next = newNode(hash, key, value, null);/*** TREEIFY_THRESHOLD:将链表转换为红黑树的阈值(默认为8),超过该阈值执行 treeifyBin 方法* 注意:执行 treeifyBin 方法并不代表一定会将链表转换为红黑树,它会根据 table 的总长度来决定,即:* 只有当 table 的长度大于等于 64 后,才会执行转换红黑树操作,否则只会对 table 进行扩容*/if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 这里会判断整条链表上的结点的key、Hash值与插入的元素的key、Hash值是否相等(前面只判断了链表中的第一个结点 p)// 如果相等,同前面一样,表示已经存在key值相同的结点【e = p.next,其中 e 已经赋值了】,则直接退出循环if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) {V oldValue = e.value;/*** public V put(K key, V value) {*     return putVal(hash(key), key, value, false, true);* }* 这里 onlyIfAbsent 为 false,则 !onlyIfAbsent 为 true,进而执行 e.value = value 【新值替换旧值】*/if (!onlyIfAbsent || oldValue == null)e.value = value;/*** 在HashMap中:void afterNodeAccess(Node<K,V> p) { }* 实际上,afterNodeAccess 是提供给LinkedHashMap类【继承HashMap】使用,LinkedHashMap 可以保证输入输出顺序的一致性* 类似的还有 afterNodeInsertion、afterNodeRemoval 这两个方法*/afterNodeAccess(e); // 这里默认实现为空return oldValue;}}// ...return null;
} 

putVal 方法中还涉及一些其他方法,如:

  1. resize:初始化或加倍表大小。如果 table 为空,则会根据字段阈值中保持的初始容量目标进行分配。
  2. treeifyBin:判断是否需要将链表替换为红黑树
    a. replacementTreeNode:将链表结点 Node 转换为树结点 TreeNode
    b. treeify:形成从该节点链接的节点的树
  3. Node:链表结点
  4. TreeNode:红黑树结点,关于红黑树的实现及对应操作,后续有机会再另讲

putVal 方法流程图(仅用于辅助理解源码):
在这里插入图片描述

其他

关于其他的方法,如 get、entrySet、ketSet 等,都可以在理解上述代码后再去看源码即可

关于红黑树:需要首先理解红黑树概念,再回头来看这里的源码更有效果(TODO:红黑树及HashMap红黑树源码理解)

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}/*** Returns root of tree containing this node.*/final TreeNode<K,V> root() {for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}}// ...省略几百行
}

补充1:HashSet

HashSet 的底层实现是 HashMap,来看 HashSet 的无参构造器:

/*** Constructs a new, empty set; the backing <tt>HashMap</tt> instance has* default initial capacity (16) and load factor (0.75).*/
public HashSet() {map = new HashMap<>();
}

add 方法:

public boolean add(E e) {return map.put(e, PRESENT)==null;
}

这里是把传入的值,作为 map 的 key 传入,而 value 统一为 PRESENT【private static final Object PRESENT = new Object();】

HashSet 之所以可以保证数据不会重复,其关键在于调用了 HashMap 的 put 方法:

// ...
if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length; // 初始化
if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null); // 通过hash计算【add】方法传入的e的下标i,若在table[i]不存在则构建结点保存
else {// 如果结点存在,则判断 key 与 hash 值是否都相同,具体流程此处不再赘述Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// ...
}// ...

也就是说,HashSet 利用 HashMap 中 key 值不能重复的特性来保证其存入的值不会重复。

HashSet 中其他方法基本都基于 HashMap 的方法,此处不再赘述。

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

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

相关文章

V90 PN伺服驱动器附加报文750详细使用介绍(算法分析)

1、V90PN伺服驱动器转矩控制(750报文) V90 PN伺服驱动器转矩控制(750报文)_v90pn转矩控制-CSDN博客文章浏览阅读3.4k次,点赞2次,收藏3次。主要介绍通过标准报文加附加报文 750 实现发送驱动报文的控制字、速度给定、转矩限幅及附加转矩给定的功能,首先就是V90在博途环境下…

分享5款.NET开源免费的Redis客户端组件库

前言 今天大姚给大家分享5款.NET开源、免费的Redis客户端组件库&#xff0c;希望可以帮助到有需要的同学。 StackExchange.Redis StackExchange.Redis是一个基于.NET的高性能Redis客户端&#xff0c;提供了完整的Redis数据库功能支持&#xff0c;并且具有多节点支持、异步编…

总结2024/6/3

省流&#xff0c;蓝桥杯国优&#xff0c;还是太菜了&#xff0c;听说都是板子题但是还是写不出来&#xff0c;靠暴力好歹没有爆0&#xff0c;还是得多练&#xff0c;明年加油了

!力扣 108. 将有序数组转换为二叉搜索树

给你一个整数数组 nums &#xff0c;其中元素已经按升序排列&#xff0c;请你将其转换为一棵 平衡二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输出&#xff1a;[0,-3,9,-10,null,5] 解释&#xff1a;[0,-10,5,null,-3,null,9] 也将被视为正确答案…

封装了一个使用UICollectionViewLayout 实现的吸附居左banner图

首先查看效果图 实现的原理就是通过自定义UICollectionView layout&#xff0c;然后 设置减速速率是快速就可以达到吸附的效果 _collectionView.decelerationRate UIScrollViewDecelerationRateFast; 下面贴出所有代码 这里是.h // // LBMiddleExpandLayout.h // Liubo…

文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《具有源荷不平衡特性的配电网智能软开关和储能联合规划》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

CTF_RE学习

学了一个 map&#xff08;&#xff09;函数的使用 import base64rawData "e3nifIH9b_CndH" target list(map(ord, rawData)) # map 函数将 rawData 中的每个字符传递给 ord 函数。ord 函数返回给定字符的 Unicode 码点 print(target) # 打印 map 对象的内存地址&…

MySQL数据库的约束

MySQL对于数据库存储的数据, 做出一些限制性要求, 就叫做数据库的"约束". 在每一列的 列名, 类型 后面加上"约束". 一. not null (非空) 指定某列不能存储null值. 二. unique (唯一) 保证这一列的每行必须有唯一值. 我们可以看到, 给 table 的 sn 列插…

【微服务】docker部署redis,一主二从三哨兵,读写分离

配置redis读写分离 3台虚拟机 创建目录用于挂载 mkdir -p /root/redis/{conf,data,logs} #master配置文件 bind 0.0.0.0 //任何ip都能访问 port 6379 //redis端口号 logfile "/data/redis.log" //日志文件存放位置&#xff0c;启动redis之前设置为空&#xff…

prometheus docker部署

1.安装Docker sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"registry-mirrors":["https://hub-mirror.c.163.com"] } EOF export DOWNLOAD_URL"https://hub-mirror.163.com/docker-ce" curl -fsSL https://ge…

【LIN】STM32新能源汽车LIN通信实现过程

【LIN】STM32新能源汽车LIN通信实现过程 文章目录 前言一、软件二、接线图三、硬件原理图四、上位机五、PICO示波器串行解码1.软件中的LIN波特率设置-192002.PIC设置3.PIC串行解码 六.引用总结 前言 【电机控制】直流有刷电机、无刷电机汇总——持续更新 使用工具&#xff1a;…

godot.bk

1.搜索godot国内镜像&#xff0c;直接安装&#xff0c;mono是csharp版本 2.直接解压&#xff0c;50m&#xff0c;无需安装&#xff0c;直接运行 3.godot里分为场景&#xff0c;节点 主场景用control场景&#xff0c;下面挂textureact放背景图片&#xff0c;右键实例化子场景把…

961题库 北航计算机 计算机网络 附答案 简答题形式

有题目和答案&#xff0c;没有解析&#xff0c;不懂的题问大模型即可&#xff0c;无偿分享。 第1组 习题 某网络拓扑如题下图所示&#xff0c;其中 R 为路由器&#xff0c;主机 H1&#xff5e;H4 的 IP 地址配置以及 R 的各接口 IP 地址配置如图中所示。现有若干以太网交换机…

MySQL—函数—函数小结

一、引言 前面博客我们已经学完了MySQL的函数&#xff0c;下面快速的对MySQL的函数做一个小结。 在讲解了MySQL的函数的时候&#xff0c;主要有四个方面&#xff1a; 1、字符串函数 &#xff08;1&#xff09;CONCAT&#xff1a;字符串连接 &#xff08;2&#xff09;LOWER、…

Java 多线程创建:三种主要方法

多线程编程是Java中一个重要的技术点&#xff0c;它允许程序并行执行多个任务&#xff0c;从而提高程序的执行效率。本文将详细介绍在Java中创建多线程的三种主要方法&#xff1a;继承Thread类、实现Runnable接口以及使用Callable和Future接口。 1. 继承 Thread 类 继承Threa…

Ubuntu server 24 (Linux) IPtables 双网卡 共享上网NAT 安装配置DHCP

一 开启路由转发功能 sudo vim /etc/sysctl.conf net.ipv4.ip_forward1 sudo sysctl -p 二 安装DHCP #更新软件包列表&#xff1a; sudo apt update #安装DHCP服务器 sudo apt install isc-dhcp-server #修改监听网卡,根据实际修改 sudo vi /etc/default/isc-dhcp-server …

配置 HTTP 代理 (HTTP proxy)

配置 HTTP 代理 [HTTP proxy] 1. Proxies2. curl2.1. Environment2.2. Proxy protocol prefixes 3. Use an HTTP proxy (使用 HTTP 代理)3.1. Using the examples (使用示例)3.1.1. Linux or macOS3.1.2. Windows Command Prompt 3.2. Authenticating to a proxy (向代理进行身…

Tailwindcss Layout布局相关样式及实战案例,5万字长文,附完整源码和效果截图

aspect 相关样式类 基础样式 ClassPropertiesaspect-autoaspect-ratio: auto;aspect-squareaspect-ratio: 1 / 1;aspect-videoaspect-ratio: 16 / 9; 案例&#xff1a;引入B站视频 Use the aspect-* utilities to set the desired aspect ratio of an element. 使用’ asp…

两款 IntelliJ IDEA 的 AI 编程插件

介绍两款 IntelliJ IDEA 的 AI 编程插件&#xff1a;通义灵码和 CodeGeeX。 通义灵码 这是由阿里推出的一个基于通义大模型的 AI 编码助手。 它提供了代码智能生成、研发智能问答等功能。通义灵码经过海量优秀开源代码数据训练&#xff0c;可以根据当前代码文件及跨文件的上下…

kafka-偏移量图解

生产者偏移量&#xff1a;生产者发送消息时写入到哪个位置&#xff08;主题的每个分区会存储一个 leo 即将写入消息的偏移量&#xff09;&#xff0c;每次写完消息 leo 会 1 消费者偏移量&#xff1a;消费者从哪个位置开始消费消息&#xff0c;小于等于 leo&#xff0c;每个组…