HashMap 的 put 原理及 JDK7 与 JDK8 对比

HashMap 的 put 原理及 JDK7 与 JDK8 对比文档

一、HashMap 的 put 方法核心原理

HashMap 是 Java 中常用的键值对存储结构,基于哈希表实现,其put方法的核心作用是将键值对 (key-value) 存储到集合中。主要流程包括:

  1. 计算哈希值:根据 key 的 hashCode () 计算哈希值,用于确定元素在数组中的存储位置
  2. 确定桶位置:通过哈希值计算数组索引(桶位置)
  3. 处理哈希冲突:当多个元素映射到同一桶位置时,使用链表或红黑树存储
  4. 扩容机制:当元素数量达到阈值时,对哈希表进行扩容以保证性能

二、JDK7 与 JDK8 中 HashMap 的 put 方法对比

特性

JDK7

JDK8

数据结构

数组 + 单向链表

数组 + 单向链表 + 红黑树

哈希计算

多次扰动处理

简化扰动处理,保留高位信息

链表插入方式

头插法

尾插法

扩容时链表迁移

原顺序倒置

保持原顺序

阈值判断

先判断是否需要扩容,再插入

先插入,再判断是否需要扩容

冲突解决优化

仅链表

链表长度 > 8 时转为红黑树

并发问题

可能产生循环链表

避免了循环链表,但仍非线程安全

三、JDK8 中 HashMap 的 put 方法源码注释

/**

* 在此映射中关联指定值与指定键。如果该映射以前包含了一个该键的映射关系,

* 则旧值被替换。

*

* @param key 与指定值相关联的键

* @param value 与指定键相关联的值

* @return 以前与 key 相关联的值,如果没有 key 的映射关系,则返回 null。

* (返回 null 还可能表示该映射以前将 null 与 key 相关联。)

*/

public V put(K key, V value) {

// 调用hash(key)计算键的哈希值,然后调用putVal方法

return putVal(hash(key), key, value, false, true);

}

/**

* 计算键的哈希值,用于分布到数组中

* 对key的hashCode进行扰动处理,减少哈希冲突

*/

static final int hash(Object key) {

int h;

// 如果key为null,哈希值为0,否则将key的hashCode与自身右移16位进行异或运算

// 目的是混合哈希码的高位和低位,增加低位的随机性,减少冲突

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

/**

* put方法的实际实现

*

* @param hash key的哈希值

* @param key 键

* @param value 要放入的值

* @param onlyIfAbsent 如果为true,则不改变已存在的值

* @param evict 如果为false,表示处于创建模式

* @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;

 

// 1. 初始化哈希表(table为null或长度为0时进行初始化)

if ((tab = table) == null || (n = tab.length) == 0)

// 调用resize()方法进行初始化,返回初始化后的数组长度n

n = (tab = resize()).length;

 

// 2. 计算桶位置并检查是否为空

// (n - 1) & hash 等价于 hash % n,但位运算效率更高

if ((p = tab[i = (n - 1) & hash]) == null)

// 如果桶为空,直接在该位置创建新节点

tab[i] = newNode(hash, key, value, null);

else {

// 3. 桶不为空,处理哈希冲突

Node<K,V> e; K k;

 

// 3.1 检查桶中第一个节点的key是否与当前key相同

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

// 如果key相同,记录下该节点,后续用于替换值

e = p;

// 3.2 检查当前节点是否是红黑树节点

else if (p instanceof TreeNode)

// 如果是红黑树,则调用红黑树的putTreeVal方法插入节点

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

else {

// 3.3 不是红黑树,遍历链表

for (int binCount = 0; ; ++binCount) {

// 到达链表尾部

if ((e = p.next) == null) {

// 在链表尾部插入新节点(尾插法,与JDK7的头插法不同)

p.next = newNode(hash, key, value, null);

// 检查链表长度是否达到阈值(8),如果达到则将链表转为红黑树

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

treeifyBin(tab, hash);

break;

}

// 找到相同的key,退出循环

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

 

// 4. 如果找到了相同的key(e != null)

if (e != null) { // existing mapping for key

V oldValue = e.value;

// 根据onlyIfAbsent参数决定是否替换旧值

if (!onlyIfAbsent || oldValue == null)

e.value = value;

// 访问后回调,用于LinkedHashMap实现

afterNodeAccess(e);

// 返回旧值

return oldValue;

}

}

 

// 5. 没有找到相同的key,增加修改次数

++modCount;

 

// 6. 检查是否需要扩容

if (++size > threshold)

resize();

 

// 插入后回调,用于LinkedHashMap实现

afterNodeInsertion(evict);

// 没有旧值,返回null

return null;

}

四、JDK8 中 HashMap 的 put 方法核心改进点解析

  1. 引入红黑树:当链表长度超过 8 时,自动转换为红黑树,将查询时间复杂度从 O (n) 优化为 O (log n),极大提升了大量哈希冲突时的性能
  2. 尾插法插入:相比 JDK7 的头插法,避免了多线程环境下可能出现的链表循环问题,同时保持了链表的插入顺序
  3. 哈希计算优化:简化了哈希计算的扰动次数,通过将高 16 位与低 16 位异或,在保证哈希分布均匀性的同时提高了计算效率
  4. 扩容机制优化:扩容时通过高低位拆分的方式迁移节点,避免了 JDK7 中重新计算哈希值的操作,同时保持了链表的原有顺序
  5. 流程调整:先插入元素再判断是否需要扩容,与 JDK7 的先判断后插入不同,逻辑更清晰

这些改进使得 JDK8 的 HashMap 在性能和稳定性上都有了显著提升,尤其是在处理大量数据和哈希冲突较多的场景下表现更为优异。

流程步骤详解

计算键的哈希值

首先调用hash(key)方法计算键的哈希值,该方法对 key 的 hashCode 进行扰动处理,以减少哈希冲突:
static final int hash(Object key) {
    int h;
    // 对key的hashCode进行高位与低位的异或运算,增加哈希值的随机性
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

 

作用:通过混合哈希码的高位和低位信息,使哈希值的分布更均匀,减少后续哈希冲突的概率。
 

计算数组索引位置

根据哈希值计算元素在数组中的存储位置(索引):
int n = table.length;
int index = (n - 1) & hash;

 

作用:通过混合哈希码的高位和低位信息,使哈希值的分布更均匀,减少后续哈希冲突的概率。
 

 检查索引位置是否为空

获取数组中索引位置的节点,如果该位置为空,则直接创建新节点并插入:
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

 

作用:处理无哈希冲突的情况,直接存储元素。
 

处理哈希冲突

当索引位置不为空时,表示发生哈希冲突,需要进一步处理:
 

 检查节点 key 是否相同

首先检查当前位置的节点 key 是否与插入的 key 相同(哈希值相同且 equals 返回 true):
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;  // 记录该节点,用于后续值替换

 

作用:处理键已存在的情况,准备替换其对应的值。
 

检查是否为红黑树节点

如果不是相同的 key,检查当前节点是否为红黑树节点:
else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

 

作用:如果当前位置是红黑树结构,则调用红黑树的插入方法。
 

遍历链表处理

如果既不是相同 key,也不是红黑树节点,则遍历链表:
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;
    }
    // 找到相同key的节点
    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}

作用:在链表中查找是否存在相同 key 的节点,不存在则在尾部插入新节点,并检查是否需要树化。

 

 替换已有键的值

如果找到相同 key 的节点(e != null),则替换其值:
if (e != null) { 
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;  // 替换值
    afterNodeAccess(e);  // 用于LinkedHashMap的回调
    return oldValue;     // 返回旧值
}

作用:处理键已存在的情况,替换其对应的值并返回旧值。

更新元素数量并检查扩容

如果是新插入的节点(不是替换已有节点),则更新修改次数和元素数量,并检查是否需要扩容:
++modCount;  // 更新修改次数
if (++size > threshold)  // 检查是否超过阈值
    resize();  // 执行扩容
afterNodeInsertion(evict);  // 用于LinkedHashMap的回调
return null;  // 新插入节点返回null

作用:维护 HashMap 的状态,并在必要时进行扩容以保证性能。

 树化检查

在链表插入新节点后,会检查是否需要将链表转换为红黑树:
if (binCount >= TREEIFY_THRESHOLD - 1)
    treeifyBin(tab, hash);

treeifyBin方法中,会先检查数组容量是否足够(≥64),不足则先扩容,否则进行树化:

if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();  // 容量不足,先扩容
else
    // 执行树化操作

 作用:当链表过长时,将其转换为红黑树,将查询时间复杂度从 O (n) 优化为 O (log n)。

posted @ 2025-09-02 11:26  诸葛匹夫  阅读(39)  评论(0)    收藏  举报