HashMap的Get(),Put()源码解析
1、什么是 HashMap?
HashMap 是 Java 中用于存储键值对(Key-Value)的集合类,它实现了 Map 接口。其核心特点是:
无序性:不保证元素的存储顺序,也不保证顺序恒定不变。
唯一性:键(Key)不能重复,若插入重复键会覆盖原有值。
允许 null:允许一个 null 键和任意数量的 null 值。
非线程安全:相比 HashTable,HashMap 不支持同步,性能更高。
2. 核心数据结构:哈希表(HashTable)
HashMap 的底层是一个 哈希表,本质是一个 数组 + 链表 / 红黑树 的复合结构:
- 数组:也称为 “哈希桶”(Bucket Array),每个位置称为一个 “桶”(Bucket)。
- 链表 / 红黑树:当多个键通过哈希函数映射到同一个桶时,这些键值对会以链表或树的形式存储。
3、get()源码及分析
get()方法是 HashMap 中用于获取指定键对应值的核心方法.
源码如下:
public V get(Object key) {Node<K,V> e;// 调用getNode方法获取节点,若节点存在则返回其值,否则返回nullreturn (e = getNode(hash(key), key)) == null ? null : e.value;
}
/*** 实现Map.get及其相关方法的核心逻辑* * @param hash 键的哈希值(通过hash(key)计算得到)* @param key 要查找的键* @return 对应的节点,如果不存在则返回null*/
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 检查哈希表是否存在且不为空,以及对应的桶是否有节点//这里tab[(n - 1) & hash是根据键的哈希值 hash,计算其在哈希表数组 tab 中的索引位置//当 n 是 2 的幂时,(n - 1) & hash 等价于 hash % nif ((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)))) // 键相等(引用相等或equals为true)return first; // 找到匹配的键,返回第一个节点// 如果第一个节点不匹配且有后续节点if ((e = first.next) != null) {//当链表长度超过 8 且数组长度超过 64 时,链表会转换为红黑树// 如果是红黑树节点,调用红黑树的查找方法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);}}// 未找到匹配的键,返回nullreturn null;
}
get方法流程
- 计算键的哈希值和数组索引。
- 检查对应桶的首节点:
- 若首节点的键匹配,直接返回。
- 若首节点是树节点,调用红黑树的查找方法。
- 否则遍历链表查找。
注意!
- 通过hash(key)方法计算键的哈希值,该方法会将键的原始 hashCode 与其高 16 位进行异或操作,以减少哈希冲突。
- 使用(n - 1) & hash计算数组索引,其中n是数组长度(始终为 2 的幂),这种方式等价于取模运算但效率更高。
4、put()源码及分析
put() 是 HashMap 最核心的方法之一,用于存储键值对。
源码如下:
public V put(K key, V value) {// 调用 putVal 方法,传入键的哈希值、键、值等参数return putVal(hash(key), key, value, false, true);
}/*** 实现 Map.put 及其相关方法的核心逻辑* * @param hash 键的哈希值* @param key 键* @param value 值* @param onlyIfAbsent 如果为 true,则不覆盖已存在的值* @param evict 如果为 false,表示处于创建模式(用于 LinkedHashMap)* @return 旧值(如果存在),否则返回 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;// 检查数组是否为空或长度为0,若是则初始化数组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; // 移动到下一个节点}}// 步骤6:处理键已存在的情况if (e != null) { // 键已存在V oldValue = e.value;// 根据 onlyIfAbsent 参数决定是否更新值if (!onlyIfAbsent || oldValue == null)e.value = value;// LinkedHashMap 的回调方法(HashMap 中为空实现)afterNodeAccess(e);return oldValue; // 返回旧值}}// 步骤7:记录修改次数,检查是否需要扩容++modCount;if (++size > threshold)//扩容resize();// LinkedHashMap 的回调方法(HashMap 中为空实现)afterNodeInsertion(evict);return null; // 键不存在,返回 null
}
put方法流程
- 计算键的哈希值和数组索引。
- 如果对应桶为空,直接插入新节点。
- 如果桶中已有节点:
- 若首节点的键匹配,覆盖其值。
- 若首节点是树节点,调用红黑树的插入方法。
- 否则遍历链表,找到相同键则覆盖,未找到则插入新节点(链表长度≥8 时树化)。
4.插入后检查是否需要扩容。
注意!
首次插入时,数组默认长度为 16。扩容条件,键值对数量超过阈值(容量 × 负载因子
)。扩容步骤,数组长度翻倍(如 16 → 32)。