HashMap源码解读,扩容操作

HashMap内部结构
jdk8以前:数组+链表
jdk8以后:数组+链表+红黑树 (当链表长度到 8 且table的大小 >= 64时,转化为红黑树,)

在这里插入图片描述

数组结构:

transient Node<K,V>[] table;

链表结构:
在这里插入图片描述

 HashMap<String, Double> hashmap = new HashMap<>();
      map.put("k1",0.1);
      map.put("k2",0.2);
      map.put("k3",0.3);
      map.put("k4",0.4);

构造函数:
在这里插入图片描述
向hashmap中添加第一个元素:
在这里插入图片描述
然后进入 hash 函数中

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

发现它并不仅仅是取 key 对应的hashCode

(h = key.hashCode()) ^ (h >>> 16) 按位异或,把hashCode算术右移16位

在这里我简单的理解是为了使分布更加均匀,尽量让不同的 key 对应不同的 hash 值,避免哈希碰撞。

然后就进入了最核心的函数 putVal 函数

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;   //定义了一些辅助变量
        System.out.println(threshold);

        if ((tab = table) == null || (n = tab.length) == 0)   //table就是上面说的HashMap的数组结构 Node[]
            
            //此时进行第一次扩容
            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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

putVal方法执行流程图:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

在这里插入图片描述

面试题:如果在new HashMap的时候,传入的size是300,则添加300个元素的时候,HashMap总共经过几次扩容?

答案是一次都没有。

分析过上面的源码,那么解决这个问题就很简单了,首先默认容量传入的是300,那么HashMap会先被处理成2的倍数,也就是2的9次幂 512,那么当前HashMap的实际容量其实是 512。
根据512 * 0.75 = 384。也就是说,下次扩容应该是当容量达到384时。

所以默认给300个size时是不需要扩容的。

根据阿里巴巴Java开发手册,集合在初始化时,最好指定好集合的初始值大小
在这里插入图片描述

面试题:

说⼀下HashMap的Put⽅法

先说HashMap的Put⽅法的⼤体流程:

  1. 根据Key通过哈希算法与与运算得出数组下标
  2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是 Node对象)并放⼊该位置
  3. 如果数组下标位置元素不为空,则要分情况讨论
    a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对 象,并使⽤头插法添加到当前位置的链表中
    b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
    i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过 程中会判断红⿊树中是否存在当前key,如果存在则更新value
    ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插 ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊ 到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成红⿊树
    iii. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就 扩容,如果不需要就结束PUT⽅法

Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?

  1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询 整体效率
  2. 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断 链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
  3. 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就 是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希算法, 节省CPU资源
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
HashMap是Java中的一种数据结构,提供了键值对的存储和查找功能。在HashMap的底层实现中,使用了数组和链表(或者在Java 1.8中使用了红黑树)来解决哈希冲突的问题。 哈希冲突指的是当不同的键对象计算出的哈希值相同时,它们需要被存储在数组的同一个位置上。为了解决哈希冲突,HashMap中使用了两种方法,分别是开放地址法和链地址法。 开放地址法是指当发生哈希冲突时,继续寻找下一个空槽位来存储键值对。这个方法需要保证数组的长度是2的幂次方,通过hash & (length-1)的位运算来减少哈希冲突的概率[2]。 链地址法是指将发生哈希冲突的键值对存储在同一个位置上的链表或红黑树中。这个方法在Java 1.8中使用,当链表的长度超过一定阈值时,会将链表转换为红黑树,以提高查找效率。 在HashMap中,put方法用于插入键值对。当调用put方法时,首先会计算键对象的哈希值,并与数组的长度取余来确定存储位置。如果该位置已经存在键值对,则根据键对象的equals方法来判断是否是同一个键,如果是,则更新对应的值,否则将新键值对插入到链表或红黑树中。如果发生哈希冲突,就会根据选择的解决冲突的方法,继续寻找下一个空槽位或者在链表或红黑树中插入键值对。如果插入后,数组中存储的键值对的数量超过了负载因子(默认为0.75),就会触发扩容操作扩容操作会创建一个更大的数组,并将原数组中的键值对重新计算哈希值后插入到新数组中。扩容操作会在数组大小达到阈值(数组长度乘以负载因子)时触发。 总结起来,HashMap的底层实现是通过数组和链表(或红黑树)来解决哈希冲突的问题。它使用哈希值计算和位运算来确定存储位置,同时使用开放地址法和链地址法来解决哈希冲突。在插入键值对时,需要计算哈希值、确定存储位置,并根据解决冲突的方法进行插入。当数组中的键值对数量超过负载因子时,会触发扩容操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [HashMap 底层解读(一行一行读,有基础就能看懂)](https://blog.csdn.net/rain67/article/details/124043769)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小本科生debug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值