• 当添加一个键值对元素时,HashMap发生了什么?


    目录

    引言

    HashMap存储结构

    调用put()方法添加元素

    HashMap什么时候扩容?


     

    引言

    HashMap实现了Map接口,它是双列集合中的一员,与单列集合不同的是,单列集合中只能存储某一类元素,它是一组值,而HashMap存储的是一个个key-value键值对,由一个键映射一个value。HashMap是无序,键唯一的。

    HashMap存储结构

    • HashMap内部数据结构使用数组+链表+红黑树进行存储。

    • 数组:数组类型为Node[],默认长度为16,每个Node都保存了某个KV键值对元素的key、value、hash、next等值。由于next的存在,所以每个Node对象都是一个单向链表中的组成节点。
    • 链表:当新添加一个KV键值对元素时,通过该元素的key的hash值,计算该元素在数组中应该保存的下标位置。如果该下标位置如果已经存在其它Node对象(产生哈希冲突),则采用链地址法处理,即将新添加的KV键值对元素将以链表形式存储。将新元素封装成一个新的Node对象,插入到该下标位置的链表尾部(尾插法)。
    • 红黑树:当链表的长度超过8并且数组长度大于64时,为了避免查找搜索性能下降,该链表会转换成一个红黑树。

    调用put()方法添加元素

    1.通过HashMap对象调用put()方法,在这个方法中调用putVal()方法。

    1. public V put(K key, V value) {
    2. return putVal(hash(key), key, value, false, true);
    3. }
    4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    5. boolean evict) {
    6. Node[] tab; Node p; int n, i;
    7. if ((tab = table) == null || (n = tab.length) == 0)
    8. n = (tab = resize()).length;
    9. if ((p = tab[i = (n - 1) & hash]) == null)
    10. tab[i] = newNode(hash, key, value, null);
    11. else {
    12. Node e; K k;
    13. if (p.hash == hash &&
    14. ((k = p.key) == key || (key != null && key.equals(k))))
    15. e = p;
    16. else if (p instanceof TreeNode)
    17. e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
    18. else {
    19. for (int binCount = 0; ; ++binCount) {
    20. if ((e = p.next) == null) {
    21. p.next = newNode(hash, key, value, null);
    22. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    23. treeifyBin(tab, hash);
    24. break;
    25. }
    26. if (e.hash == hash &&
    27. ((k = e.key) == key || (key != null && key.equals(k))))
    28. break;
    29. p = e;
    30. }
    31. }
    32. if (e != null) { // existing mapping for key
    33. V oldValue = e.value;
    34. if (!onlyIfAbsent || oldValue == null)
    35. e.value = value;
    36. afterNodeAccess(e);
    37. return oldValue;
    38. }
    39. }
    40. ++modCount;
    41. if (++size > threshold)
    42. resize();
    43. afterNodeInsertion(evict);
    44. return null;
    45. }

    2.在putVal()方法中首先调用hash()方法,将键key传入这个方法。重新计算这个键的哈希值。并将这个哈希值传入putVal()方法。这个参数是决定元素存储位置的关键因素之一。

    1. static final int hash(Object key) {
    2. int h;
    3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    4. }

    3.在 putVal()方法中,判断数组是否为空,如果为空则将数组进行初始化。

    4.如果不为空,用传入putVal()方法中的hash(即上文中调用hash()方法算出),通过数组长度(n-1)&hash 计算元素应当存放在Node数组中的下标index。

    5.查看table[index]中是否有数据。

    6.如果没有数据,就将要存入的key,value封装成一个Node节点。存放在table[index]中。

     

     

    7.如果存在数据,说明发生哈希冲突,继续判断key是否相同。如果相同,用新的value替换原数据。

    8.如果不相同,判断当前节点是不是TreeNode树型的节点。

    9.如果是 树形节点,就将key,value封装成树型节点插入红黑树中。

    10.如果不是树型节点, 将key,value封装成一个Node节点。链接在链表尾部。

     

    11.判断链表长度是否大于8,并且数组长度大于64,如果满足,就将链表转换为红黑树;如果不满足,数组扩容;

     

     

    12.元素添加完成,判断当前节点数是否大于实际存储空间的大小;

    13.如果大于,调用resize()方法,按照原数组的长度,扩容一倍(扩容后容量小于或等于64)。然后将原数组的内容复制至新数组。

     

     扩容:

     复制:

    HashMap什么时候扩容?

    在添加元素的过程中,势必会造成HashMap扩容,什么时候扩容呢?

    • 当HashMap中的元素个数超过数组大小 x 加载因子(LoadFactor)时,就会进行数组扩容,加载因子默认是0.75。
    • 加入元素时,如果链表长度大于8,并且数组长度小于64,会进行数组扩容。
    • 加入元素时,如果链表长度大于8,并且数组长度等于64,会将链表转换为红黑树。

    每次扩容,元素都会重新散列。

     

  • 相关阅读:
    数值分析学习笔记——绪论【华科B站教程版本】
    阿里云函数计算 1024云上见 AIGC小说创作大赛:如何搭建自定义的阿里通义千问
    DPU网络开发SDK——DPDK(一)
    马克 · 扎克伯格期望的元宇宙到底会是什么样子?
    R语言提交后台任务Rstudio\nohup
    Redis-性能优化
    为什么标准AR HUD的FOV必须在10°×3°以上|技术科普
    Python学习笔记——基本类型、函数、输入和输出
    多路复用补充
    网络编程详细介绍()
  • 原文地址:https://blog.csdn.net/m0_59340907/article/details/126337211