当前位置: 首页 > news >正文

HashMap的源码学习

学习 Java 中 HashMap 的源码是深入理解哈希表数据结构、Java 集合框架设计思想的重要途径。下面从核心原理、关键属性、核心方法实现等方面,带你逐步剖析 JDK 8 及以上版本的 HashMap 源码(注:JDK 8 是 HashMap 实现的重要转折点,引入了红黑树优化,以下分析以 JDK 8 为基础)。

一、HashMap 核心原理概览

HashMap 是基于 哈希表 实现的 Map 接口,存储键值对(key-value),允许 keyvaluenullkey 仅允许一个 null),且无序(插入顺序与遍历顺序不一致)。

其核心思想是:

  1. 哈希函数:通过 keyhashCode() 计算哈希值,确定元素在数组中的存储位置(桶索引)。
  2. 解决哈希冲突:当多个 key 计算出相同的桶索引时,JDK 8 采用 链表 + 红黑树 结合的方式:
    • 当链表长度 <= 8 时,用链表存储(查询时间复杂度 O(n))。
    • 当链表长度 > 8 时,转为红黑树(查询时间复杂度 O(log n))。
  3. 动态扩容:当元素数量(size)超过 负载因子(loadFactor)× 数组长度(capacity) 时,触发扩容(数组长度翻倍),重新计算所有元素的存储位置(rehash)。

二、关键属性与常量

先看 HashMap 类中定义的核心属性和常量,理解其底层存储结构的基础:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {// 1. 常量// 默认初始容量(必须是 2 的幂):16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16// 最大容量(2^30)static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载因子:0.75(减少哈希冲突的概率)static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链表转红黑树的阈值:当链表长度 > 8 时转树static final int TREEIFY_THRESHOLD = 8;// 红黑树转链表的阈值:当树节点数 < 6 时转链表(避免频繁转换)static final int UNTREEIFY_THRESHOLD = 6;// 最小树化容量:当数组长度 < 64 时,即使链表长度超 8,也先扩容而非转树static final int MIN_TREEIFY_CAPACITY = 64;// 2. 核心存储结构:哈希桶数组(数组 + 链表/红黑树)// 类型是 Node<K,V>[],Node 是链表节点;若转树则为 TreeNode(继承 Node)transient Node<K,V>[] table;// 3. 其他关键属性// 元素数量(key-value 对的个数)transient int size;// 结构修改次数(用于迭代器的快速失败机制)transient int modCount;// 扩容阈值(= capacity × loadFactor),当 size 超过此值时触发扩容int threshold;// 负载因子(可在构造方法中指定)final float loadFactor;
}
  • table 数组HashMap 的核心存储容器,每个元素是一个 Node 节点(链表节点)或 TreeNode 节点(红黑树节点)。
  • 容量(capacity)table 数组的长度,必须是 2 的幂(原因后面解释),默认 16。
  • 负载因子(loadFactor):控制哈希表的疏密程度,默认 0.75(平衡空间和时间效率)。
  • 阈值(threshold):触发扩容的临界值,初始为 DEFAULT_INITIAL_CAPACITY × DEFAULT_LOAD_FACTOR = 12

三、哈希函数与桶索引计算

HashMap 中,key 的存储位置(桶索引)由两步计算得出,目的是减少哈希冲突:

1. 计算 key 的哈希值(hash() 方法)
static final int hash(Object key) {int h;// 若 key 为 null,哈希值为 0;否则取 key 的 hashCode(),并将高 16 位与低 16 位异或return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 作用:将 keyhashCode()(32 位整数)的高 16 位与低 16 位进行异或,混合哈希值的高位和低位,减少因低位重复导致的哈希冲突(尤其在数组长度较小时,高位无法参与索引计算,通过异或让高位信息融入低位)。
2. 计算桶索引(i = (n - 1) & hash)

通过哈希值计算数组索引时,HashMap 没有用取模运算(hash % n),而是用:

int index = (table.length - 1) & hash;
  • 原因:当 n(数组长度)是 2 的幂时,n - 1 的二进制是全 1(如 n=16 时,n-1=151111),此时 (n-1) & hash 等价于 hash % n,但位运算效率更高。
  • 这也解释了为什么 capacity 必须是 2 的幂:保证索引计算的均匀性,避免某些位置永远无法被使用。

四、核心方法解析

1. 构造方法

HashMap 有三个主要构造方法,用于初始化容量和负载因子:

// 1. 无参构造:使用默认容量(16)和默认负载因子(0.75)
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他属性默认初始化
}// 2. 指定初始容量:使用默认负载因子
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}// 3. 指定初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);// 初始容量最大不超过 MAXIMUM_CAPACITY(2^30)if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " + loadFactor);this.loadFactor = loadFactor;// 计算初始阈值:将 initialCapacity 向上调整为最接近的 2 的幂(如 initialCapacity=10 → 16)this.threshold = tableSizeFor(initialCapacity);
}// 辅助方法:返回 >= 给定值的最小 2 的幂(用于初始化容量)
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;
}
  • 注意:table 数组(哈希桶)并非在构造方法中初始化,而是在第一次插入元素时(put 方法)才真正初始化(延迟初始化,节省空间)。
2. put() 方法(核心中的核心)

put 方法用于添加键值对,流程较复杂,可分为以下步骤:

public V put(K key, V value) {// 调用 putVal 方法,传入 key 的哈希值、key、value,其他参数默认return putVal(hash(key), key, value, false, true);
}/*** 真正执行插入的核心方法* @param hash key 的哈希值* @param key 键* @param value 值* @param onlyIfAbsent 若为 true,则仅当 key 不存在时才插入(不覆盖已有值)* @param evict 用于 LinkedHashMap 的标志,HashMap 中无实际意义* @return 旧值(若 key 已存在)或 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;// 步骤 1:若 table 未初始化(null 或长度 0),则初始化 table(resize())if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 步骤 2:计算桶索引 i = (n-1) & hash,若桶为空(p == null),直接新建节点放入if ((p = tab[i]) == null)tab[i] = newNode(hash, key, value, null);else { // 步骤 3:桶不为空(存在哈希冲突)Node<K,V> e; K k;// 3.1 若桶中第一个节点的 hash 和 key 与当前 key 相同,记录该节点(e = p)if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;// 3.2 若桶是红黑树(p 是 TreeNode),则调用树的插入方法else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 3.3 若桶是链表,遍历链表else {for (int binCount = 0; ; ++binCount) {// 3.3.1 若遍历到链表尾部(e = p.next == null),新建节点插入尾部if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 若链表长度超过 TREEIFY_THRESHOLD(8),则尝试转红黑树(treeifyBin)if (binCount >= TREEIFY_THRESHOLD - 1) // -1 因为从 0 开始计数treeifyBin(tab, hash);break;}// 3.3.2 若链表中存在相同 key,跳出循环(e 已记录该节点)if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))break;p = e; // 移动到下一个节点}}// 步骤 4:若存在相同 key 的节点(e != null),则覆盖旧值if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e); // 空方法,供 LinkedHashMap 重写return oldValue;}}// 步骤 5:若插入了新节点,修改次数 +1,检查是否需要扩容++modCount;if (++size > threshold)resize();afterNodeInsertion(evict); // 空方法,供 LinkedHashMap 重写return null;
}

put 方法核心流程总结

  1. 若哈希桶数组未初始化,先调用 resize() 初始化。
  2. 计算桶索引,若桶为空,直接插入新节点。
  3. 若桶不为空:
    • 若第一个节点与当前 key 相同,直接覆盖。
    • 若桶是红黑树,调用树的插入方法。
    • 若桶是链表,遍历链表:
      • 若找到相同 key,覆盖旧值。
      • 若未找到,插入链表尾部,若长度超 8 则尝试转红黑树(转树前会检查数组长度,若 <64 则先扩容)。
  4. 插入新节点后,若 size 超过阈值,调用 resize() 扩容。
3. resize() 方法(扩容)

resizeHashMap 中最复杂的方法之一,负责初始化哈希桶或在容量不足时扩容(数组长度翻倍),并重新计算所有元素的存储位置(rehash)。

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 情况 1:旧数组已初始化(oldCap > 0)if (oldCap > 0) {// 若旧容量已达最大值(2^30),则阈值设为 Integer.MAX_VALUE,不再扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 否则,新容量 = 旧容量 × 2(仍为 2 的幂),新阈值 = 旧阈值 × 2else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}// 情况 2:旧数组未初始化,但旧阈值 > 0(通常是通过带参构造方法指定了初始容量)else if (oldThr > 0)newCap = oldThr;// 情况 3:旧数组未初始化,且旧阈值 = 0(无参构造),使用默认值else {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;// 创建新的哈希桶数组(容量为 newCap)@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 若旧数组不为 null,将旧数组中的元素转移到新数组中if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null; // 帮助 GC// 若桶中只有一个节点,直接计算新索引并放入新数组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 { // preserve orderNode<K,V> loHead = null, loTail = null; // 索引不变的链表(low 链)Node<K,V> hiHead = null, hiTail = null; // 索引 = 旧索引 + 旧容量的链表(high 链)Node<K,V> next;do {next = e.next;// 核心:通过 hash & oldCap 判断新索引(旧容量是 2 的幂,二进制只有一位为 1)// 若结果为 0,新索引 = 旧索引;否则新索引 = 旧索引 + oldCapif ((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);// 将 low 链放入新数组的旧索引位置if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 将 high 链放入新数组的(旧索引 + oldCap)位置if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}

resize 核心逻辑

  • 扩容后容量为原来的 2 倍(保证仍是 2 的幂)。
  • 转移旧元素到新数组时,利用 hash & oldCap 判断新索引:
    • 若结果为 0,新索引 = 旧索引(low 链)。
    • 若结果非 0,新索引 = 旧索引 + 旧容量(high 链)。
    • 无需重新计算所有节点的哈希,效率更高。
  • 红黑树在转移时可能拆分为两个链表或保持树结构(若节点数不足则转链表)。
4. get() 方法

get 方法用于根据 key 获取对应的 value,流程相对简单:

public V get(Object key) {Node<K,V> e;// 调用 getNode 方法,传入 key 的哈希值和 keyreturn (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 && ((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);}}return null; // 未找到
}

get 流程

  1. 计算 key 的哈希值和桶索引。
  2. 若桶为空,返回 null
  3. 若桶不为空:
    • 检查第一个节点是否匹配,匹配则返回。
    • 若为红黑树,调用树的查找方法。
    • 若为链表,遍历链表查找匹配节点。

五、红黑树相关(TreeNode)

JDK 8 中,TreeNode 继承自 Node,并实现了红黑树的结构(平衡二叉树的一种),用于优化长链表的查询效率。核心方法包括:

  • putTreeVal:红黑树的插入。
  • getTreeNode:红黑树的查找。
  • split:扩容时红黑树的拆分。
  • 红黑树的旋转(rotateLeftrotateRight)和着色调整,保证树的平衡。

红黑树的实现较为复杂,核心是通过 节点颜色(红/黑)旋转操作 维持树的平衡,确保查询、插入、删除的时间复杂度为 O(log n)。

六、总结与注意事项

  1. 线程不安全HashMap 是非线程安全的,多线程环境下可能导致死循环(扩容时)或数据不一致,需使用 ConcurrentHashMap 替代。
  2. 迭代器快速失败modCount 记录结构修改次数,迭代过程中若结构被修改,会抛出 ConcurrentModificationException
  3. key 的哈希值不可变:若 key 是自定义对象,需重写 hashCode()equals(),且保证 key 的哈希值在存储期间不变(否则可能无法找到元素)。
  4. 性能影响因素:初始容量和负载因子影响性能,容量过小会导致频繁扩容,过大则浪费空间;负载因子过高会增加哈希冲突概率,过低则空间利用率低。

通过阅读源码,不仅能理解 HashMap 的工作原理,还能学习到哈希表设计、冲突解决、动态扩容等经典算法思想,以及 Java 中的优化技巧(如位运算、延迟初始化等)。建议结合调试工具,跟踪 putresize 等方法的执行过程,加深理解。

http://www.dtcms.com/a/550986.html

相关文章:

  • 建设银行网站特点分析php网站怎么搭建环境配置
  • 长春设计网站肥西县建设局官方网站
  • php网站开发实例教程代码网站设计大全
  • 如何做好阿里巴巴企业网站建设网站界面设计总结
  • 电商网站建设多少钱wordpress定制主题开发
  • Spring Bean作用域与生命周期全解析
  • 选择邯郸网站制作南昌网站建设策划
  • 邢台市政建设集团网站上传照片的网站赚钱
  • 扩展阅读:数据标注的两种类型 - 矩形框标注 和 关键点标注
  • 小杰-大模型(one)——大模型的概念与历程。
  • 为什么用开源建站第三方商城网站开发
  • 政务移动门户网站建设方案php开源cms
  • 重庆建设教育网站昭通网站建设
  • 企业申请网站建设请示3秒钟自动跳转网页
  • 加强网站建设的措施网站营销推广应该怎么做
  • 做网站看网页效果999网站免费
  • TypeScript 中的 args 详解,和 arguments 有什么不同?
  • 鞍山网站设计公司免费刷推广链接的软件
  • 做网站用什么语言简单制作动画的网站
  • 三坐标同轴度测量方法
  • 汕头多语种网站制作俄罗斯做牙网站
  • 仿织梦小说网站源码浙江网
  • 做教程网站如何查用户搜索深圳福田区十强企业
  • 二叉搜索树学习笔记
  • 建设部安全员证书查询网站妇科在线医生免费咨询
  • 网站建设zrhskjseo如何优化排名
  • 河北建设执业资格注册中心网站中国十大杰出建筑师
  • 【Rust编程】ORM框架Diesel
  • 呢图网站场建设封面桔子摄影
  • 设计网站大全湖南岚鸿网站大全网站大事记时间轴折叠