HashMap 实现原理

article/2025/11/9 10:23:49

简介

本文为我对 HashMap 实现原理的笔记整理以及一些个人理解,如若发现有错误的地方,欢迎留言指正

在不同的 Java 版本中 HashMap 的实现也略有不同,本文示例使用的 Java 版本为:“1.8.0_181”

什么是 Hash(散列函数)

Hash 音译为「哈希」,它是把任意长度的输入通过散列算法变换成固定长度的输出,这个输出称为散列值。

这种转换是一种压缩映射,也就是说散列值的空间远小于输入的空间,不同的输入可能会散列成相同的输出,所以 不可能从散列值来确定唯一的输入值

在这里插入图片描述

Map: key 和 value 的映射

在了解了什么是 Hash 后,我们再来看下什么是 Map

Map 是一种最基础的数据结构,它描述的是一个 key 和 一个 value 一 一对应的数据结构。

每种高级语言都有对 Map 的定义和实现。

Map 接口的定义

Map.java

public interface Map<K,V> {···// 增加一个键值对V put(K key, V value);// 传入一个 key,并返回对应的 value// 注意此处传入的 key 的类型不同于 put 方法,不是泛型而是一个 ObjectV get(Object key);// 删除一个键值对V remove(Object key);···}
问题思考为什么 Map 中 put 方法中传入的 key 的类型是泛型,而在 get 或 remove 方法中却是 Object 类型呢? 

HashMap的实现:数组+链表+(红黑树)

HashMap 是 Map 最常用的实现类,它是通过 key 的 hash 值来决定 value 在数组中的位置。但是上面我们也提到哈希值是有可能会重复的,也就是说不同的 key 得到的哈希值是一样的,这种情况我们称为 哈希冲突

哈希冲突 意味着不同的 value 要放在数组的同一个位置,那么 HashMap 是如何处理这种情况的呢?

在 Java1.7 中 HashMap 主要以数组+链表的形式实现的,而到了 Java1.8 则引入了黑红树算法来优化 HashMap 的查询速度。

当发生 哈希冲突 时,HashMap 会将相同的 value 以链表的形式存放到对应的数组位置中。

HashMap原理图

如上图所示,table 的初始长度为 16,每个节点都由四个元素组成:hash、key、value 和 next,其中 next 记录着下一个节点。

举个例子:我们根据 key1 的哈希值得出它在 table 的下标位置为 0,若当前 table[0] 还未有元素则创建一个 Node1,这个 Node1 记录着 key1 的哈希值、key1、key1 对应的 value 值以及 next(默认为 null),然后将这个 Node1 放入 table[0] 的位置中

之后我们再根据 key2 的哈希值得到它在 table 的下标位置也为 0,此时 table[0] 已经存在了一个 Node1, 此时 HashMap 同样会先创建一个 Node2,接着把这个 Node2 放到 Node1 的 next 中

HashMap 源码分析

AbstractMap.java

public abstract class AbstractMap<K,V> implements Map<K,V> {...}

HashMap.java

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {...}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cfNEbG8h-1611126122603)(135E65BA97E24261A01521DC8A56C92B)]
HashMap 继承了 AbstractMap 抽象类,而 AbstractMap 抽象类又实现了 Map 接口

阀值、负载系数、容量

在深入 HashMap 代码之前,我们先来认识 HashMap 的几个变量

HashMap.java

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {```// 初始容量 = 2的4次方 = 16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;// 最大容量 = 2的30次方static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载系数static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链表树形化的阈值static final int TREEIFY_THRESHOLD = 8;   ```}

DEFAULT_INITIAL_CAPACITY 定义了 HashMap 的初始容量为 16

MAXIMUM_CAPACITY 则表示 HashMap 的最大容量为 230

比较有意思的是 DEFAULT_INITIAL_CAPACITY 和 MAXIMUM_CAPACITY 都是用位运算来定义的

HashMap.java

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {```// 默认负载系数static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链表树形化的阈值static final int TREEIFY_THRESHOLD = 8;   ```// 扩容阀值int threshold;// 负载系数final float loadFactor;```}

threshold 表示 HashMap 的扩容阀值,HashMap 的初始容量为 16,当 HashMap 的容量超出阀值时则进行自动扩容操作,下次扩容的容量为当前容量的两倍

loadFactor (负载系数)则是用来计算具体的阀值

阀值 = 负载系数(默认0.75) * 容量

threshold = loadFactor * capacity

简单打个比方:一开始我们用 16mL 的瓶子装水,当瓶子里的水超出 12mL 时,这时我们就将 16mL 瓶子里的水倒进 32mL 的瓶子里,用 32mL 的瓶子继续装水。

16mL 就是初始容量,32mL 就是扩容后的容量,12mL 则是阀值。

HashMap 构造函数

    public HashMap() {// 指定负载系数为默认值this.loadFactor = DEFAULT_LOAD_FACTOR;}// 指定初始容量public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}// 放入已有的 Mappublic HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}// 指定初始容量和负载系数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;// tableSizeFor 方法返回大于输入参数且最近的2的整数次幂的数// 如:输入 10 返回 16this.threshold = tableSizeFor(initialCapacity);}

HashMap 中有四个构造函数,可以看到它们并没有一开始就初始化数组

初始化数组的操作是在放入数据时,也就是 put 方法中触发的

HashMap 中的 put 方法

HashMap.java

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {...public V put(K key, V value) {// 此处先调用了 hash 方法,得到 key 的 hash 值return putVal(hash(key), key, value, false, true);}...static final int hash(Object key) {int h;// 扰动处理:一次位运算 + 一次异或// 目的是为了尽可能的防止哈希冲突return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}...final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {...}...}

HashMap 中 putVal 方法除了前面的 hash、key、value 之外,后面还有两个形参 onlyIfAbsent 和 evict

其中 onlyIfAbsent 是用来判断当传入的 key 已存在时是否允许覆盖旧值,传 false 表示不允许覆盖

HashMap.java

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {...final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {// tab:Node 数组// p:已存在的 Node// n:数组长度// i:数组的下标位置Node<K,V>[] tab; Node<K,V> p; int n, i;// 判断数组是否为空,若为空则进行初始化操作// 此处的 table 是一个 Node 数组if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 将 key 的哈希值和(n - 1)进行"与"运算,得到下标位置 i// 判断 tab[i] 是否为 null if ((p = tab[i = (n - 1) & hash]) == null) // p = tab[i]// p 为 null 则说明当前数组的位置还未有值// 新建 Node 并指向 tab[i]tab[i] = newNode(hash, key, value, null);else {// e:已存在的 Node,Node 中的 key 与传入的 key 相同Node<K,V> e; K k;// 判断 key 是否已存在if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode) // 判断 p 是否继承于树形节点e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {// 循环查找节点for (int binCount = 0; ; ++binCount) {// 获取下一节点并判断是否为 nullif ((e = p.next) == null) {// 若下一节点为 null 则在当前节点的 next 上创建新的节点p.next = newNode(hash, key, value, null);// 判断是否超出默认长度,若超出则采用红黑树// TREEIFY_THRESHOLD:8if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 判断 key 是否已存在if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// e 不为 null 则说明 key 已存在if (e != null) {V oldValue = e.value;// 若 onlyIfAbsent 为 false 或 oldValue 为 null 则进行覆盖操作// 若 onlyIfAbsent 为 true 则不进行覆盖。可参考 putIfAbsent 方法if (!onlyIfAbsent || oldValue == null)// 将新的值覆盖到旧的值e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;// 判断是否需要扩容// 若元素个数大于阀值则进行扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}...}

HashMap 中的 resize 方法

putVal 方法中我们可以看到 HashMap 在初始化或扩容时都调用了 resize 方法

接下来我们具体看下 resize 方法是如何实现的

HashMap.java

    // 用于初始化或扩容表大小// 每次以二的倍数进行扩容final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {// 若 table 长度已经等于或大于最大容量if (oldCap >= MAXIMUM_CAPACITY) {// 将 table 的阀值设为 int 的最大值threshold = Integer.MAX_VALUE;// 返回旧数组return oldTab;}// newCap // 新的 table 长度比原有多一倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // 若阈值大于0,则将 table 初始容量等于阈值newCap = oldThr;else {               // 若阈值小于等于0,则将 table 初始容量等于默认值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;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // 定义两个链:一个 lo 链,一个 hi 链Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 如果 (e.hash & oldCap) == 0 则插入 lo 链中if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else { // 否则插入 hi 链中if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;// 插入到 j 的位置newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;// 插入到 j + oldCap 的位置newTab[j + oldCap] = hiHead;}}}}}return newTab;}

http://chatgpt.dhexx.cn/article/lYJRQnbe.shtml

相关文章

HashMap底层实现原理(看这一篇就够了)

HashMap概述 HashMap实现了Map接口&#xff0c;我们常用HashMap进行put和get操作读存键值对数据。下面介绍基于jdk1.8深入了解HashMap底层原理。 HashMap数据结构 HashMap实际是一种“数组链表”数据结构。在put操作中&#xff0c;通过内部定义算法寻止找到数组下标&#xf…

HashMap的实现原理及其特点

1) HashMap可以接受null键值和值&#xff0c;而HashTable则不能&#xff0c;HashMap是非synchronized的&#xff1b;存储的是键值对。 2) HashMap是基于hashing原理,使用put(key,value)存储对象到HashMap中&#xff0c;使用get(key)从HashMap中获取对象&#xff0c;当我们给pu…

说一下HashMap的实现原理?

点击上方蓝色“趣学程序”&#xff0c;选择“设为星标” 回复“资源”获取独家整理的学习资料&#xff01; 回复“加群”与更多小伙伴共同成长&#xff01; 回复“源码”获取专属项目源码&#xff01; 本文会对java集合框架中的对应实现HashMap的实现原理进行讲解&#xff0c;然…

HashMap的实现原理和底层数据结构

看了下JAVA里面有HashMap、Hashtable、HashSet三种hash集合的实现源码&#xff0c;这里总结下&#xff0c;理解错误的地方还望指正 HashMap和Hashtable的区别 HashSet和HashMap、Hashtable的区别 HashMap和Hashtable的实现原理 HashMap的简化实现MyHashMap HashMap和Hashtable的…

HashMap底层实现原理

HashMap实现原理 1.概述 HashMap是基于哈希表的Map接口的非同步实现。元素以键值对的形式存放&#xff0c;并且允许null键和null值&#xff0c;因为key值唯一&#xff08;不能重复&#xff09;&#xff0c;因此&#xff0c;null键只有一个。另外&#xff0c;hashmap不保证元素…

hashMap实现原理

1. HashMap概述&#xff1a; HashMap是基于哈希表的Map接口的非同步实现&#xff08;Hashtable跟HashMap很像&#xff0c;唯一的区别是Hashtalbe中的方法是线程安全的&#xff0c;也就是同步的&#xff09;。此实现提供所有可选的映射操作&#xff0c;并允许使用null值和null键…

HashMap的实现原理和底层结构

哈希表&#xff08;hash table&#xff09;也叫散列表&#xff0c;是一种非常重要的数据结构&#xff0c;应用场景及其丰富&#xff0c;许多缓存技术&#xff08;比如memcached&#xff09;的核心其实就是在内存中维护一张大的哈希表&#xff0c;而HashMap的实现原理也常常出现…

【java】HashMap底层实现原理

目录 一.哈希表(散列)1.什么是哈希表2.什么是哈希冲突(面试题)3.解决哈希冲突的方法(面试题)(1) 开放地址法① 线性探查②二次探查③随机探查 (2) 再哈希法(3) 链地址法(4)建立公共溢出区 二.HashMap1.HashMap的hash()算法(面试)(1)为什么不是h key.hashCode()直接返回&#x…

HashMap底层实现原理解析

一、HashMap底层实现原理解析 我们常见的有数据结构有三种结构&#xff1a; 数组结构链表结构哈希表结构 下面我们来看看各自的数据结构的特点&#xff1a; 1&#xff09;数组结构&#xff1a; 存储区间连续、内存占用严重、空间复杂度大 优点&#xff1a;随机读取和修改效…

HashMap底层实现原理概述

1. 前言 在一场面试中最能打动面试官的其实是细节&#xff0c;候选人对细节的了解程度决定了留给面试官的印象到底是“基础扎实”还是“基础薄弱”&#xff0c;如果候选人能够举一反三主动阐述自己对一些技术细节的理解和总结&#xff0c;那无疑是面试过程中的一大亮点。HashM…

HashMap实现原理分析

之前转载过一篇HashMap相关分析文章&#xff0c;快速链接&#xff1a;HashMap实现原理分析 既然有前辈已经将源码分析总结了出来&#xff0c;我们在继续学习研究源码实现的时候不妨借鉴借鉴前人的总结与经验~ 本文转自&#xff1a;https://blog.csdn.net/hefenglian/article/…

HashMap面试,看这一篇就够了

历史热门文章&#xff1a; 七种方式教你在SpringBoot初始化时搞点事情 Java序列化的这三个坑千万要小心可以和面试官聊半个小时的volatile原理Java中七个潜在的内存泄露风险&#xff0c;你知道几个&#xff1f;JDK 16新特性一览啥&#xff1f;用了并行流还更慢了InnoDB自增原理…

java中HashMap原理

1、为什么用HashMap&#xff1f; HashMap是一个散列桶&#xff08;数组和链表&#xff09;&#xff0c;它存储的内容是键值对(key-value)映射HashMap采用了数组和链表的数据结构&#xff0c;能在查询和修改方便继承了数组的线性查找和链表的寻址修改HashMap是非synchronized&a…

HashMap的实现原理

1. HashMap概述 HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作&#xff0c;并允许使用null值和null键。此类不保证映射的顺序&#xff0c;特别是它不保证该顺序恒久不变。 在java编程语言中&#xff0c;最基本的结构就是两种&#xff0c;一个…

软件测试用例分析和用例设计

测试用例的概念 测试用例&#xff08;test case&#xff09;&#xff0c;也叫测试案例&#xff0c;是为了达到一个最佳的测试效果或者高效的发现软件中的隐藏错误&#xff08;缺陷&#xff09;而精心设计的包括场景步骤和数据。 通用的定义&#xff1a;是关于一个功能验证时候…

软件测试用例设计练习

1、完成163邮箱注册用例的编写 邮箱地址&#xff1a;6~18个字符&#xff0c;可使用字母、数字、下划线&#xff0c;需要以字母开头 密码&#xff1a;8~16个字符&#xff0c;大、小写字母、数字、标点符号&#xff0c;3种或以上组合 2、需求&#xff1a;输入三条边&#xff…

软件测试用例详细规范

软件测试用例详细规范 为什么编写测试用例详细测试用例模板测试用例字段介绍用例操作步骤用例预期结果&#xff1a; 测试用例录入原则&#xff1a;测试用例设计步骤测试用例案例&#xff1a;测试用例校验点&#xff1a; 为什么编写测试用例 我也不知道&#xff0c;自己百度 详…

软件测试——测试用例设计方法

1、测试用例定义 测试用例又叫test case&#xff0c;是为某个特殊目标而编制的一组测试输入&#xff0c;执行条件以及预期结果&#xff0c;以便测试某个程序路径或核实是否满足某个特定需求。 2、测试用例的特性 有效性&#xff1a;测试用例能够被使用&#xff0c;且被不同人…

接口测试用例怎么写?

测试流程 需求规格说明书--测试计划--测试用例--用例评审--开发 接口文档--接口分析--接口用例设计--评审--接口测试执行&#xff08;关注数据库&#xff09;--前后端对接 系统界面测试--测试结束 如何设计接口测试用例&#xff1f; 1.接口正常调用&#xff0c;先要能跑通…

软件测试 - 用例篇

回顾上一篇博客主要内容:软件测试 - 基础篇 1: 软件测试的流程是什么? 需求分析,测试计划,测试设计/测试开发,测试执行,测试报告 需求分析 分析需求,验证需求的正确性和合理性,从需求中提取出测试项 测试计划 要考虑测试人数,测试环境,测试时间,测试设备等 测试设计/测试开发 …