HashMap的Get(),Put()源码解析

1、什么是 HashMap?

HashMap 是 Java 中用于存储键值对(Key-Value)的集合类,它实现了 Map 接口。其核心特点是:

无序性:不保证元素的存储顺序,也不保证顺序恒定不变。

唯一性:键(Key)不能重复,若插入重复键会覆盖原有值。

允许 null:允许一个 null 键和任意数量的 null 值。

非线程安全:相比 HashTableHashMap 不支持同步,性能更高。

2. 核心数据结构:哈希表(HashTable)

HashMap 的底层是一个 哈希表,本质是一个 数组 + 链表 / 红黑树 的复合结构:
  • 数组:也称为 “哈希桶”(Bucket Array),每个位置称为一个 “桶”(Bucket)。
  • 链表 / 红黑树:当多个键通过哈希函数映射到同一个桶时,这些键值对会以链表或树的形式存储。

3、get()源码及分析

get()方法是 HashMap 中用于获取指定键对应值的核心方法.
源码如下
public V get(Object key) {
    Node e;
    // 调用getNode方法获取节点,若节点存在则返回其值,否则返回null
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
 * 实现Map.get及其相关方法的核心逻辑
 * 
 * @param hash 键的哈希值(通过hash(key)计算得到)
 * @param key 要查找的键
 * @return 对应的节点,如果不存在则返回null
 */
final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    
    // 检查哈希表是否存在且不为空,以及对应的桶是否有节点
    //这里tab[(n - 1) & hash是根据键的哈希值 hash,计算其在哈希表数组 tab 中的索引位置
    //当 n 是 2 的幂时,(n - 1) & hash 等价于 hash % n
    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))))  // 键相等(引用相等或equals为true)
            return first;  // 找到匹配的键,返回第一个节点
        
        // 如果第一个节点不匹配且有后续节点
        if ((e = first.next) != null) {
            //当链表长度超过 8 且数组长度超过 64 时,链表会转换为红黑树
            // 如果是红黑树节点,调用红黑树的查找方法
            if (first instanceof TreeNode)
                return ((TreeNode)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);
        }
    }
    
    // 未找到匹配的键,返回null
    return null;
}
get方法流程
  1. 计算键的哈希值和数组索引。
  2. 检查对应桶的首节点:
  • 若首节点的键匹配,直接返回。
  • 若首节点是树节点,调用红黑树的查找方法。
  • 否则遍历链表查找。
注意!
  • 通过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[] tab; Node 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 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)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方法流程
  1. 计算键的哈希值和数组索引。
  2. 如果对应桶为空,直接插入新节点。
  3. 如果桶中已有节点:
  • 若首节点的键匹配,覆盖其值。
  • 若首节点是树节点,调用红黑树的插入方法。
  • 否则遍历链表,找到相同键则覆盖,未找到则插入新节点(链表长度≥8 时树化)。

     4.插入后检查是否需要扩容。

注意!

        首次插入时,数组默认长度为 16。扩容条件,键值对数量超过阈值(容量 × 负载因子)。扩容步骤,数组长度翻倍(如 16 → 32)。

你可能感兴趣的:(哈希算法,散列表,算法)