HashMap 的 put() 方法执行过程分析:究竟发生了什么?

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

在 Java 编程中,HashMap 是最常用的集合类之一,它提供了基于键值对的高效存储和查找功能。你是否曾经好奇过,当我们调用 put() 方法往 HashMap 中插入元素时,究竟发生了什么?今天,我就带你深度解析 HashMapput() 方法执行过程,揭开背后的神秘面纱!

1. put() 方法的基本概述

put() 方法是 HashMap 中用来插入或更新元素的核心方法。它的签名如下:

V put(K key, V value);
  • key:存储的键
  • value:存储的值
  • 返回值:put() 方法会返回键对应的旧值。如果该键之前没有对应的值,则返回 null

如果键已经存在,则会更新该键对应的值;如果键不存在,则会插入一个新的键值对。

2. put() 方法的执行过程

HashMap 中,put() 方法的执行过程可以分为几个关键步骤,我们通过下图来简要总结每个步骤的内容:

步骤 1:计算哈希值

put() 方法首先会计算插入的键的哈希值。HashMap 是基于哈希表实现的,因此哈希值决定了键值对的存储位置。哈希值的计算使用了键的 hashCode() 方法,它返回一个整数作为哈希值。

但是,hashCode() 可能会存在哈希冲突的情况(即不同的键可能具有相同的哈希值)。为了尽量避免冲突,HashMap 会对哈希值做进一步的处理(通过扰动函数)来减少冲突的概率。

int hash = hash(key.hashCode());

步骤 2:定位桶(Bucket)

哈希值计算出来后,HashMap 会通过以下公式将其映射到具体的桶(即数组的索引)上:

int index = (n - 1) & hash; // n 是当前数组的长度,通常是 2 的幂

通过这个公式,HashMap 确定了哈希表中存储键值对的具体位置。此时,如果该位置为空(即没有其他元素占用该桶),put() 方法会直接将新的键值对插入到该位置。

步骤 3:判断是否有哈希冲突

如果该桶已经有元素了(哈希冲突的情况),那么 put() 方法会开始检查冲突元素的链表或红黑树结构。

  • 链表法:如果哈希冲突较轻,HashMap 会使用链表来存储多个键值对,所有哈希值相同的元素会在同一个桶中以链表的形式存储。
  • 红黑树法:如果同一个桶中的元素数量超过一定阈值(通常是 8),HashMap 会将链表转换成红黑树,以提高查找效率。

在链表或红黑树中,put() 方法会遍历现有的元素,判断当前要插入的键是否与链表或红黑树中的元素相同。

步骤 4:更新或插入元素

  • 如果遍历到一个与新键相同的元素(即存在相同的键),那么 put() 方法会更新该键对应的值,并返回旧值。
  • 如果没有找到相同的键,put() 方法就会在该桶的链表或红黑树的末尾插入新的键值对。

步骤 5:扩容检查

每当元素数量达到负载因子(默认是 0.75)与当前容量的乘积时,HashMap 会触发扩容操作。扩容会将当前的容量扩展到原来的两倍,并重新计算所有元素的存储位置。扩容时,所有现有的元素都会重新计算哈希值并放入新的桶中,这一过程会导致性能下降,所以 put() 方法会尽量避免频繁的扩容操作。

扩容的触发条件如下:

if (size > threshold) {
    resize();
}

其中,threshold 是负载因子与当前容量的乘积,size 是当前元素的数量。

3. put() 方法的源码解析

为了更深入理解 put() 方法的实现,以下是 HashMapput() 方法的简化版源码分析:

public V put(K key, V value) {
    // 1. 计算哈希值
    int hash = hash(key.hashCode());

    // 2. 获取桶的索引
    int index = (n - 1) & hash;

    // 3. 获取对应桶
    Node<K, V> first = table[index];

    // 4. 如果该桶为空,直接插入
    if (first == null) {
        table[index] = newNode(hash, key, value, null);
        size++;
        if (size > threshold)
            resize();
        return null;
    }

    // 5. 如果该桶非空,查找是否存在相同键
    Node<K, V> node = first;
    do {
        if (node.hash == hash && (key == node.key || key.equals(node.key))) {
            // 如果找到相同键,更新值
            V oldValue = node.value;
            node.value = value;
            return oldValue;
        }
        node = node.next;
    } while (node != null);

    // 6. 没有找到相同键,插入新元素
    table[index] = newNode(hash, key, value, first);
    size++;
    if (size > threshold)
        resize();
    return null;
}

在这段代码中:

  • hash():计算并扰动哈希值。
  • index:根据哈希值和桶的大小计算出元素应存放的位置。
  • 如果桶为空,直接创建新的节点。
  • 如果桶非空,遍历链表(或红黑树)检查是否有相同键的元素,若有则更新值,若没有则插入新元素。
  • 在每次插入或更新后,都会检查是否需要扩容。

4. put() 方法的性能分析

put() 方法的性能受到多个因素的影响:

  • 哈希冲突:当多个键映射到同一个桶时,查找、插入的时间复杂度会从 O(1) 增加到 O(n),其中 n 是桶中元素的数量。为了减少哈希冲突,HashMap 会通过扰动函数和扩容机制来尽量保持均匀分布。
  • 扩容:扩容是一个代价较大的操作,因为需要重新计算所有元素的哈希值,并将它们重新分配到新的桶中。因此,频繁的扩容会影响性能。合理的负载因子和容量设置能有效减少扩容的次数。

总结

通过上面的分析,我们可以看到,HashMapput() 方法是一个复杂的过程,涉及到哈希值计算、桶定位、哈希冲突处理、元素插入和扩容等多个步骤。尽管如此,得益于其合理的设计,HashMap 在平均情况下能够提供 O(1) 的时间复杂度。然而,哈希冲突、扩容等因素仍然可能导致性能下降,因此合理选择容量和负载因子至关重要。

理解了 put() 方法的实现过程,你就能更好地掌握 HashMap 的性能优化技巧,并在实际开发中做出更好的选择!

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值