浅显易懂HashMap的数据结构
HashMap 就像一个大仓库,里面有很多小柜子(数组),每个小柜子可以挂一串链条(链表),链条太长的时候会变成更高级的架子(红黑树)。下面用超简单的例子解释:
壹. 排列方式
- 数组:一排固定的小柜子(比如柜子0、柜子1、柜子2...)。
- 链表:如果多个东西要放在同一个柜子里,它们会串成一条链子。
- 红黑树:当某个柜子的链子太长(比如超过8个),链子会变成一个小架子(树结构),找东西更快。
贰. 存数据的过程
假设我们要存一个键值对 ("name", "小明"):
- 找柜子:先算 "name"的哈希值(类似身份证号),比如得到柜子3。
- 放数据: - 如果柜子3是空的,直接放进去。
- 如果柜子3已经有东西,检查链条上的每个元素: - 如果有相同的键(比如已经有 "name"),替换它的值。
- 如果没有,把新键值对挂到链子末尾。
 
- 如果有相同的键(比如已经有 
 
- 链条转架子:如果链子长度超过8,就把链子变成红黑树架子。
叁. 取数据的过程
假设要取 "name" 对应的值:
- 找柜子:算 "name"的哈希值,确定是柜子3。
- 找数据: - 如果柜子3是链子,遍历链子找 "name"。
- 如果柜子3是架子(红黑树),用树的快速查找方法。
 
- 如果柜子3是链子,遍历链子找 
肆. 代码简化版(Java)
// 小柜子(数组)
Node[] table = new Node[16];// 链表节点(如果链子太长,会变成 TreeNode)
class Node {String key;String value;Node next; // 下一个节点
}// 红黑树节点(架子结构)
class TreeNode extends Node {TreeNode parent;TreeNode left;TreeNode right;
}// 存数据示例
void put(String key, String value) {int hash = key.hashCode();int index = hash % table.length; // 计算柜子位置if (table[index] == null) {// 柜子是空的,直接放进去table[index] = new Node(key, value);} else {// 柜子非空,挂到链子末尾或更新值// 如果链子太长,转成红黑树}
}伍. 一句话总结
HashMap 的数组是主干,链表解决哈希冲突,红黑树解决链表过长时的性能问题!
陆. 源码:HashMap的putVal()方法
/*** Implements Map.put and related methods** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/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);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}柒、我们来拆解 “哈希冲突时,链表如何挂到数组上” 的详细过程,用大白话 + 例子解释:
1. 核心原理:用链表串联冲突的键值对
- 当两个不同的键(比如 "name"和"age")通过哈希计算后,分配到 同一个数组位置(同一个柜子) 时,就会发生 哈希冲突。
- 此时,HashMap 会用 链表 把这些冲突的键值对串起来,挂在数组的这个位置上(类似挂一串钥匙)。
2. 具体挂链表的步骤(逐行分析)
假设数组的某个位置 index 已经有数据,现在要存入新的键值对 (key2, value2):
-  检查是否重复:遍历链表,对比每个节点的 key:- 如果发现某个节点的 key和key2相同(用equals()判断),直接覆盖它的值。
- 如果链表中没有相同的 key,则把新节点 挂到链表末尾(Java 8 之后是尾插法)。
 
- 如果发现某个节点的 
-  链表挂载示意图: // 数组的某个位置 index 已经有一个节点 Node1 table[index] = Node1(key1, value1) -> null;// 存入新节点 Node2(冲突) Node1.next = Node2(key2, value2); // 挂到链表尾部 table[index] = Node1 -> Node2 -> null;
-  代码逻辑简化版: Node existingNode = table[index]; // 获取数组当前位置的链表头// 遍历链表,检查是否已有相同的 key while (existingNode != null) {if (existingNode.key.equals(newKey)) {existingNode.value = newValue; // 覆盖旧值return;}existingNode = existingNode.next; }// 如果没有重复 key,把新节点挂到链表末尾 Node newNode = new Node(newKey, newValue); newNode.next = table[index]; // Java 8 之前是头插法(新节点变成链表头) table[index] = newNode; // Java 8 之后是尾插法(直接挂在链表尾部)
3. 关键细节:头插法 vs 尾插法
- Java 8 之前:新节点插入链表头部(头插法)。 - 问题:多线程下可能导致链表成环(死循环)。
- 示例:table[index] = 新节点 -> 旧节点 -> null。
 
- Java 8 之后:新节点插入链表尾部(尾插法)。 - 改进:避免多线程下的链表成环问题。
- 示例:旧节点 -> 新节点 -> null。
 
4. 链表转红黑树的条件
- 当链表长度 超过 8,且 数组总长度 ≥ 64 时,链表会转换成红黑树。
- 如果数组长度 < 64,HashMap 会选择 扩容数组(而不是转树),减少哈希冲突。
5. 完整流程示例
假设存入 ("name", "小明") 和 ("age", 18),它们的哈希值冲突(都映射到数组位置 3):
-  存入第一个节点: table[3] = Node("name", "小明") -> null;
-  存入第二个节点(冲突): // 检查链表,发现没有重复 key,挂到链表末尾 table[3] = Node("name", "小明") -> Node("age", 18) -> null;
-  查找时:遍历链表,逐个对比 key,找到对应值。
6.总结
- 链表挂在数组上:通过节点的 next指针串联冲突的键值对。
- 插入位置:Java 8 之后用尾插法,避免多线程问题。
- 转红黑树:链表过长时优化查找速度(从 O(n) 优化到 O(log n))。
捌、再帮你用 “仓库管理员” 的比喻 总结一下,确保彻底理解:
终极傻瓜版总结
- 仓库(数组):管理员有一排柜子(比如16个),每个柜子有编号(0到15)。
- 存东西(键值对): - 管理员用 哈希函数(比如 key.hashCode() % 16)算出东西应该放哪个柜子。
- 如果柜子空,直接放进去。
 
- 管理员用 哈希函数(比如 
- 柜子冲突(哈希冲突): - 如果柜子已经有东西,管理员会拿一根 链条(链表) 把新东西和旧东西绑在一起,挂在柜子里。
- 链条的挂法:新东西挂在链条的尾部(Java 8之后)。
 
- 链条太长怎么办: - 如果链条长度超过8,管理员会把链条换成 高级货架(红黑树),这样找东西更快。
- 但如果仓库的柜子总数太少(比如小于64个),管理员会直接 扩建仓库(扩容数组),而不是换货架。
 
关键逻辑图示
// 数组(柜子列表)
Node[] table = [柜子0, 柜子1, ..., 柜子15];// 柜子里的内容(链表或树)
柜子3 -> Node1("name", "小明") -> Node2("age", 18) -> null  // 链表形式
柜子5 -> TreeNode("id", 1001)  // 树形式(如果链表转成了红黑树)容易混淆的点
- 覆盖值:如果两次存同一个 key(比如两次存"name"),会直接覆盖旧值。
- 哈希函数:hashCode()决定柜子位置,但不同对象可能算出相同的哈希值(冲突)。
- 扩容:当仓库的柜子被填满超过一定比例(默认75%),管理员会扩建仓库(数组长度翻倍),重新分配所有东西的位置。
玖、结合 : 面试被问 Java中hashmap数据结构
URL: 地基:Java中hashmap数据结构(面试被问)-CSDN博客
兄弟们,理解好了。rt.jar包里的hashmap源码的putval方法的方式,有厉害的同学可以学以致用哈!向大家致敬!
(望各位潘安、各位子健/各位彦祖、于晏不吝赐教!多多指正!🙏)
