HashMap源码之keyset

本文揭示了为何在HashMap遍历时推荐使用entrySet而非keySet,指出keySet可能造成两次遍历的实际行为。作者通过深入分析发现,IDEA的debug显示是toString方法触发的,entrySet的迭代器设计导致了这一现象。关键词:HashMap, entrySet, keySet, 遍历效率

HashMap遍历的时候,推荐使用entrySet,不建议使用keySet。因为使用keySet相当于对hashmap遍历了两次,一次对key的遍历,一次是根据key值又对map进行了扫描查找。
研究了一下keyset发现与我想象中的不太一样。原本以为hashmap会在put和remove中操作keyset,但是并没在put方法中找到任何关于keyset的操作。
但是idea调试时候,能看到hashmap调用put后,keyset对应的容量变化了,并且key也能在keyset找到。一头雾水还是没能找到答案。
度娘告诉我,idea调试显示的会调用其toString(),hashmap.ketSet的父类的父类AbstractCollection中toString调用了迭代器遍历集合中的值,hashmap.keyset中重写了迭代器方法,遍历集合中的key。

<think>我们正在讨论的是HashMap源码解析。用户之前已经询问了ConcurrentHashMap,现在转向HashMap。根据引用内容,用户希望获取HashMap源码的详细解析。我们需要结合引用内容,覆盖HashMap的主要方面:结构、初始化、增删改查、扩容、树化等。 注意:引用中提到了JDK1.7和1.8的不同,因为JDK1.8中HashMap引入了红黑树,所以我们将主要解析JDK1.8的HashMap。 结构: HashMap在JDK1.8中由数组+链表+红黑树组成。当链表长度超过8(且数组长度>=64)时,链表转换为红黑树;当树节点少于6时,红黑树退化为链表。 初始化: 有多个构造方法,可以指定初始容量和负载因子。如果不指定,默认初始容量为16,负载因子为0.75。构造方法中并没有直接创建数组,而是在第一次put时通过resize()方法创建。 主要方法: put: 添加键值对 get: 根据键获取值 remove: 删除键值对 resize: 扩容 treeifyBin: 将链表转换为红黑树 由于用户要求详细解析,我们将按以下步骤进行: 1. 类结构:重要的静态内部类(Node, TreeNode)和字段(table, threshold, loadFactor等) 2. 初始化过程:构造方法如何计算初始容量(tableSizeFor方法) 3. 插入过程(putVal方法)详细步骤,包括哈希计算、处理碰撞、树化判断、扩容触发等 4. 扩容机制(resize方法):如何重新分配元素 5. 树化过程(treeifyBin方法) 6. 查找(get方法)和删除(remove方法)过程 由于源码较长,我们将摘取关键部分进行解释。 注意引用[1][2][3][4][5]中的内容,我们会适当引用并标注。 根据引用[1]:我们可以从类注释开始,掌握HashMap大致框架。 引用[4]提到了构造方法中指定初始容量和负载因子,以及tableSizeFor方法。 引用[5]对比了JDK1.7和1.8的结构变化(1.7是数组+链表,1.8是数组+链表+红黑树)。 现在开始解析: </think>### HashMap 源码深度解析(JDK 1.8) #### 1. **核心结构** HashMap 由 **数组 + 链表 + 红黑树** 组成(JDK 1.8+): - **数组**:`Node<K,V>[] table`,初始长度 16,存放链表头节点 - **链表**:解决哈希冲突,节点为 `Node` 类(含 `key`, `value`, `next` 指针) - **红黑树**:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为 `TreeNode` ```java // 节点定义 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 链表指针 } // 树节点定义(继承LinkedHashMap.Entry) static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // 额外prev指针用于快速拆树 boolean red; } ``` --- #### 2. **初始化过程** ##### 2.1 构造方法 ```java // 关键参数 int threshold; // 扩容阈值 = capacity * loadFactor final float loadFactor; // 负载因子(默认0.75) // 常用构造器 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity"); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor"); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); // 计算实际容量 } ``` ##### 2.2 容量计算(`tableSizeFor`) 保证数组长度总是 **2 的幂**(便于位运算定位下标): ```java 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; } ``` > **示例**:输入 `initialCapacity=10` → 输出 `16`(大于 10 的最小 2 的幂)[^4]。 --- #### 3. **插入数据(`putVal`)** 流程如下: 1. 计算哈希值:`hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)` 2. 定位数组下标:`i = (n - 1) & hash` 3. 处理哈希冲突: - 桶为空 → 直接插入新节点 - 桶为链表 → 遍历链表插入(或更新值) - 桶为红黑树 → 调用 `TreeNode.putTreeVal()` 4. 触发扩容或树化 ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent) { 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); // CASE1:桶为空 else { // CASE2/3:桶非空(链表或树) 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) // 达到树化阈值 treeifyBin(tab, hash); break; } if (e.hash == hash && (...)) break; // 找到相同key p = e; } } if (e != null) { // 更新已存在key的值 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; return oldValue; } } if (++size > threshold) resize(); // 触发扩容 return null; } ``` --- #### 4. **扩容机制(`resize`)** 当元素数量 ≥ `threshold` 时触发: 1. 新容量 = 旧容量 × 2 2. 迁移数据(链表拆分为高低位链表): ```java // 节点迁移逻辑(链表拆分) Node<K,V> loHead = null, loTail = null; // 低位链表 Node<K,V> hiHead = null, hiTail = null; // 高位链表 do { next = e.next; // 判断节点归属(低位链表无需移动) if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { // 高位链表移动到新位置 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); ``` --- #### 5. **树化过程(`treeifyBin`)** 链表 → 红黑树的转换条件: 1. 链表长度 ≥ 8 2. 数组长度 ≥ 64(否则优先扩容) ```java final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 64 resize(); // 不满足条件时扩容 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 构建TreeNode链表 do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); // 调用红黑树平衡方法 } } ``` --- #### 6. **查找与遍历** ##### 6.1 查找(`get`) ```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; } ``` ##### 6.2 遍历(`keySet`) ```java // 遍历示例(推荐使用EntrySet迭代器) HashMap<String, String> map = new HashMap<>(); map.put("name", "Alice"); map.put("age", "30"); for (String key : map.keySet()) { String val = map.get(key); System.out.println(key + " : " + val); // 输出:name : Alice, age : 30 } ``` > **注意**:直接遍历 `keySet` 会触发多次哈希计算,建议用 `entrySet()`[^3]。 --- ### 设计要点总结 | **特性** | **JDK 1.7** | **JDK 1.8** | |------------------|--------------------------|----------------------------------| | 数据结构 | 数组+链表 | 数组+链表+红黑树 | | 哈希冲突解决 | 头插法 | 尾插法(避免死循环) | | 节点定位 | `indexFor()` | `(n-1) & hash`(位运算优化) | | 扩容后节点迁移 | 全部重新哈希 | 高低位链表拆分(性能提升 50%) | | 树化条件 | 不支持 | 链表≥8 且 数组长度≥64 |
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值