Hashmap误区

本文深入探讨了HashMap的工作原理,包括其内部结构、哈希函数的作用、冲突解决策略等,并介绍了其性能参数如初始容量和加载因子的重要性。

HashMap简介

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

 

众所周知,mashmap的原理是对key值使用哈希函数使得可以快速找到value存储的位置,因此查询效率很高。然而在很长一段时间中,我都以为hash函数是直接映射到value的内存地址,直到今天仔细地搜索了资料才纠正了这一错误的想法。

仔细看一下HashMap.class的源码,其中有一些十分重要的默认参数,比如DEFAULT_INITIAL_CAPACITY,它定义了在HashMap实例化时桶的默认大小,而其中桶就是存放键值对的容器,它定义为 Node<K,V>[] table,实际上就是一个键值对的数组,键值对以Node对象封装。关键的问题来了,每次在进行put和get操作时,HashMap根据key值计算出的hash值会对它进行二次哈希,然后再对当前容量取余,计算出一个介于0到当前容量的值,事实上索引的就是前面所说存放键值对的容器中的某个桶,这时候我不禁就会想,如果发生哈希碰撞(事实上在容量较小的时候很容易发生),在同一个桶中如何存放不止一个Node呢?这就是HashMap机智的地方了,当进行put操作,而正好桶中已经存在Node,那么就把这个Node以链表形式连接在它的下一节点。当链表容量比较大时,由于链表顺序查询的性能比较底下,HashMap更机智的设计了红黑树,当一个桶中Node节点超过一定阈值,会自动转为红黑树,当然在这种情况下,也很容易会触发哈希表的扩容。

如有错误,烦请指出!欢迎交流。

参考和引用:http://www.jb51.net/article/80443.htm

 

转载于:https://www.cnblogs.com/undefined-name/p/8698451.html

<think>嗯,用户之前问了HashMap如何判断键是否存在,现在他们想让我分析HashMap中的方法。首先,我需要确定用户的需求是什么。可能他们想深入了解HashMap的内部实现,或者是在准备面试,需要详细的知识点。用户可能是开发者,正在学习Java集合框架,或者是遇到了性能问题想优化代码。 接下来,我得考虑用户可能关心的内容。HashMap的常用方法比如put、get、remove、resize等,每个方法的实现机制是关键。例如,put方法涉及到哈希计算、解决冲突、扩容机制。用户可能想知道这些方法的底层逻辑,比如红黑树转换、负载因子影响等。 用户之前已经了解了键的存在判断,现在可能希望扩展到其他方法。需要确保覆盖主要的方法,并且解释清楚每个步骤,比如哈希计算、索引定位、链表遍历、树化过程。同时,要提到线程安全的问题,比如为什么HashMap不是线程安全的,以及ConcurrentHashMap的区别。 可能还需要对比不同JDK版本的差异,比如Java 7和Java 8中HashMap的实现变化,比如头插法改尾插法,链表转红黑树的阈值等。这些细节能帮助用户更深入理解优化点。 另外,注意事项部分需要强调正确实现hashCode和equals的重要性,以及初始化参数的影响。用户可能在开发中遇到因为错误重写这些方法导致的bug,或者因为不合理的初始容量和负载因子引起的性能问题。 还需要考虑用户可能的误区,比如为什么扩容是翻倍,负载因子为什么默认0.75,这些设计背后的权衡。另外,线程安全的替代方案,比如使用ConcurrentHashMap或者Collections.synchronizedMap,也是用户可能需要的知识点。 最后,总结部分要简明扼要,让用户速抓住重点。示例代码可以帮助用户验证理解,比如展示不同情况下的方法调用结果。确保回答结构清晰,逻辑连贯,覆盖用户可能需要的各个方面,同时避免过于冗长,保持信息的有效传递。</think>--- ### **HashMap 核心方法分析** HashMap 的核心方法围绕**哈希桶管理**、**键值对操作**和**扩容机制**展开。以下是关键方法及其实现逻辑的分析: --- ### **1. `put(K key, V value)`** **功能**:插入键值对,若键已存在则更新值。 **步骤**: 1. **计算哈希值**:调用 `hash(key)`(扰动后的哈希值)。 2. **定位桶索引**:`(n - 1) & hash`(`n`为当前数组长度)。 3. **处理冲突**: - **桶为空**:直接创建新节点。 - **桶非空**:遍历链表或红黑树: - 若找到相同键(`hash`和`equals`均匹配),更新值。 - 否则,插入新节点到链表末尾或红黑树。 4. **扩容检查**:若元素数超过阈值(`容量 × 负载因子`),触发扩容。 **关键代码(Java 8)**: ```java 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); // 链表长度≥8时转红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 更新值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; return oldValue; } } // 扩容判断 if (++size > threshold) resize(); return null; } ``` --- ### **2. `get(Object key)`** **功能**:根据键获取值,若不存在返回 `null`。 **步骤**: 1. 计算哈希值并定位桶索引。 2. 遍历链表或红黑树,匹配 `hash` 和 `equals`。 3. 若找到,返回对应值;否则返回 `null`。 **时间复杂度**: - 平均:$O(1)$ - 最坏(哈希冲突):$O(\log n)$(红黑树优化后) --- ### **3. `remove(Object key)`** **功能**:删除指定键对应的键值对。 **步骤**: 1. 定位桶并遍历链表/树,找到匹配的节点。 2. 从链表或树中移除该节点。 3. 若树节点过少(≤6),退化为链表。 --- ### **4. `resize()`** **功能**:扩容哈希表(通常翻倍),重新分配所有键值对。 **触发条件**:元素数量超过 `容量 × 负载因子`(默认负载因子 0.75)。 **关键逻辑**: 1. 新容量 = 旧容量 × 2。 2. 遍历旧表所有节点,重新计算索引: - 若节点是链表,拆分为低位链和高位链(利用 `hash & oldCap` 是否为 0)。 - 若节点是红黑树,拆分为两棵树或退化为链表。 --- ### **5. `hash()`** **功能**:对键的 `hashCode()` 进行扰动,减少哈希冲突。 **实现**: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` --- ### **6. `treeifyBin()`** **功能**:将链表转换为红黑树(当链表长度 ≥8 且数组长度 ≥64 时触发)。 **目的**:解决哈希冲突导致的链表退化问题,将查询复杂度从 $O(n)$ 优化为 $O(\log n)$。 --- ### **方法设计的关键点** 1. **哈希冲突管理**: - 链表 + 红黑树的组合平衡时间空间复杂度。 - 树化阈值(8)和退化阈值(6)通过统计概率设定。 2. **扩容优化**: - 利用 `hash & oldCap` 速判断新索引位置,避免重新计算哈希值。 3. **线程不安全**: - 多线程操作可能导致循环链表或数据丢失(需用 `ConcurrentHashMap` 替代)。 --- ### **示例验证** ```java HashMap<String, Integer> map = new HashMap<>(16, 0.75f); map.put("A", 1); // 触发首次初始化(容量16) map.put("B", 2); map.remove("A"); // 删除节点 map.get("B"); // 返回值2 ``` --- ### **注意事项** 1. **键对象的 `hashCode()` 和 `equals()`**: - 若未正确重写,会导致哈希冲突处理失效(例如,两个逻辑相等的对象返回不同的哈希值)。 2. **初始化参数**: - 合理设置初始容量和负载因子,减少扩容开销。 3. **并发场景**: - HashMap 非线程安全,多线程环境下需使用 `ConcurrentHashMap` 或同步机制。 --- ### **总结** HashMap 的方法设计围绕**高效哈希计算**、**动态扩容**和**冲突优化**展开。通过扰动函数、红黑树和智能扩容策略,在大多数场景下实现了接近 $O(1)$ 的时间复杂度。理解其内部机制有助于避免误用(如线程安全问题)和优化性能(如合理设置初始容量)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值