HashMap (详细讲解)

HashMap 是Java中常用的集合类,基于哈希表实现,用于存储键值对。在JDK1.8之前,它由数组和链表构成,利用拉链法解决冲突。JDK1.8后,当链表长度超过8,会转为红黑树,以优化搜索性能。本文深入分析了HashMap的底层数据结构、源码,包括构造方法、put和get方法,以及resize扩容机制。

1 HashMap 简介

HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考treeifyBin 方法。

1.1 底层数据结构分析

1.1.1 JDK1.8之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列HashMap 通过 key 的hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n -1) &hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

JDK 1.8 HashMap 的 hash 方法源码:

JDK 1.8 的 hash方法 相比

<think>好的,用户之前问过Java中Collection和Map的实现类,现在他想深入了解HashMap的put和get方法的源码实现过程。需要结合他提供的引用内容来详细解析。 首先,用户提供了多个引用,其中引用2、3、4、5都涉及HashMap的源码分析,尤其是put和get方法。需要确保回答结构清晰,分步骤讲解,并正确引用相关内容。 用户可能希望了解HashMap的工作原理,特别是哈希冲突处理、红黑树转换、扩容机制等。需要注意使用中文,保持技术细节的准确性,同时避免过于复杂的术语,让用户容易理解。 需要检查引用的内容,比如引用2提到的JDK1.8的hash方法,引用4中的扩容机制,引用5中的treeifyBin方法。在回答中要自然地插入这些引用,确保每个关键点都有对应的引用支持。 另外,用户可能对线程安全性、性能优化等方面感兴趣,但当前问题集中在put和get方法,所以需要专注于这两个方法的实现流程,不过可以简要提到相关的问题作为后续扩展。 要确保回答中的代码示例和步骤符合用户提供的引用内容,比如put方法中的哈希计算、索引定位、链表/红黑树处理,以及扩容条件。同时,get方法的查找过程也需要详细说明,包括如何处理哈希冲突和树节点的查找。 最后,生成的相关问题需要与HashMap的实现细节相关,帮助用户进一步探索,比如扩容机制、红黑树转换、线程安全等问题,这些问题在用户提供的引用中也有涉及,可以合理引导用户深入阅读。</think>### Java HashMap `put`与`get`方法源码解析 结合引用[^2][^3][^4][^5],以JDK8为例解析HashMap核心方法实现: --- ### 一、`put(K key, V value)`方法实现流程 **核心步骤**:计算哈希值 → 定位桶 → 处理哈希冲突 → 扩容检查 → 数据插入 #### 1. **哈希值计算** 通过扰动函数减少哈希冲突概率: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` - **目的**:将高16位与低16位异或,增强散列性[^2] #### 2. **定位桶索引** 通过`(n-1) & hash`计算索引(`n`为当前数组长度): $$ \text{索引} = \text{哈希值} \& (\text{数组长度} - 1) $$ - **原理**:位运算代替取模,提高效率[^4] #### 3. **处理哈希冲突** - **情况1**:桶为空 → 直接插入`Node` - **情况2**:桶为链表 → 遍历链表插入尾端(JDK8改为尾插法) - **情况3**:桶为红黑树 → 调用`putTreeVal`插入树节点 **链表转红黑树条件**: - 链表长度 ≥8 且 数组容量 ≥64 → 调用`treeifyBin`转换[^5] ```java if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); ``` #### 4. **扩容检查** 触发条件(满足其一): - 元素总数 > 容量 × 负载因子(默认0.75) - 链表长度 ≥8 但数组容量 <64 → 优先扩容而非转树[^5] **扩容过程**: - 新容量 = 旧容量 × 2 - 数据迁移:重新计算索引,链表可能拆分为高位链和低位链 --- ### 二、`get(Object key)`方法实现流程 **核心步骤**:计算哈希值 → 定位桶 → 遍历查找 → 返回结果 #### 1. **哈希值与索引计算** 与`put`方法相同,使用`hash()`和位运算定位桶 #### 2. **遍历查找逻辑** - **链表查找**:遍历链表,通过`equals`比较键值 - **红黑树查找**:调用`getTreeNode`在树中搜索 #### 3. **源码关键逻辑** ```java 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 && ((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; } ``` --- ### 三、关键设计解析 1. **线程安全问题** - `put`时多线程同时触发扩容可能导致环形链表(JDK7头插法问题)[^4] - `get`方法在未同步情况下可能读到中间状态数据 2. **性能优化点** - **红黑树**:链表长度≥8时转换,避免查询复杂度退化到$O(n)$ - **负载因子**:默认0.75平衡空间利用率和哈希冲突概率 --- ### 四、示例场景 **插入20个元素的扩容次数**: - 初始容量16,负载因子0.75 → 第一次扩容在插入第12个元素后(16×0.75=12) - 扩容后容量32 → 第二次扩容在插入第24个元素后(32×0.75=24) 因此插入20个元素时仅扩容1次 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值