HashMap源码面试话术总结

HashMap是Java中实现Map接口的一种数据结构,它通过数组加链表(JDK1.8后引入红黑树)的方式解决哈希冲突。HashMap的主要属性包括:数组Node[]、entrySet、size、loadFactor、threshold等。默认初始化容量为16,负载因子为0.75。当元素数量超过阈值时,HashMap会进行扩容,同时利用2次扰动函数降低哈希冲突。在JDK1.8以后,链表长度超过8时会转换为红黑树,以优化查找效率。HashMap的put方法涉及键值对的存储和冲突解决,包括链表或红黑树的插入操作。在处理哈希冲突方面,HashMap使用了链地址法和红黑树,确保数据分布均匀并提高查找效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap是顶级接口Map的一个实现类
HashMap的主要属性有
数组Node<>[],哈希表的本质的本质是一个数组,数组中的每一个元素称为一个桶,桶中存放的是键值对,而泛型传入的两个类型是键值对的类型。

Set<Map.Entry<>> entrySet,这个集合并不是用于实际存储数据,主要是针对entrySet和keySet两个视图提供支持。

int类型 size,用于统计当前集合中的元素个数。

final float loadFactor, 为当前集合的负载因子,与当前集合的容积值形成了阈值,与扩容有关,这个属性一旦赋值则将不可修改。

int threshold 阈值,主要用于扩容操作。

HashMap中还有多个常量值,DEFAULT_INITIAL_CAPACITY,顾名思义,这个是默认的初始化容积,值为16。

MAXIMUM_CAPACITY 最大容积值,为2的30次方。

以上两个常量都是通过移位运算计算得出。

DEFAULT_LOAD_FACTOR 默认的负载因子,值为0.75,float类型。

TREEIFY_THRESHOLD树化阈值,值为8。

即链表转成红黑树的阈值,在存储数据时,当桶中链表长度 > 该值时,则满足链表转换成红黑树的两个条件之一,还得继续判断当前Node数组
长度是否大于常量MIN_TREEIFY_CAPACITY最小树化容量值64,只有这两个阈值都被达到才会将桶中的链表转换为红黑树。若只是达到了树化阈值,仅仅只是代表桶内的
元素太多,集合会进行扩容处理,扩容处理后集合元素的位置可能会发生改变。

HashMap中有个静态内部类class Node<>{}这个内部类实现了Entry<>接口。其中有五个主要属性(hash、key、value、next)HahMap中存放的key/value对就被封装为Node对象,
其中key就是存放的键值,用于决定具体的存放位置;value是具体存放的数据,hash就是当前Node对象的hash值,next用于指向下一个Node节点(单向链表) 。
当调用HashMap的无参构造器时,会采用延迟初始化的策略,这样可以节约空间,给负载因子loadFactor赋默认值0.75
若传入指定的初始化容积时,会调用另外的构造器进行初始化操作,负载因子采用默认值0.75,
在真正有初始化操作的构造器中,首先会对传入的初始化容积和负载因子进行合法性验证,若不通过则抛异常(负载因子一般建议在(0,1]范围内)。
负载因子越小,哈希碰撞的概率越低,也就意味着链表长度越小,但是浪费空间;如果值越大,则节约空间,但是哈希碰撞概率提高。
然后将传入的初始化容积在传入tableSizeFor()方法中进行计算真正的初始化容积值,HashMap并不是直接使用传入的参数
作为初始化数组的长度,而是将传入的整数转换为大于等于这个数的最小的2的n次方值。这是在tableSizeFor()方法中通过移位计算等操作实现的。

为了减少hash冲突,引入了加载因子,加载因子表示容器中最多存储百分子多少的数据。例如默认容器为16,默认加载因子为0.75,则表示最多存储16*0.75个元素,如果大于这个值则需要进行扩容 。

在HashMap构造器中并没有创建任何用于存储数据的集合---延迟加载,只有在第一次存储数据时才进行空间分配。

每个Node[]数组中的元素被称一个桶bucket,一个桶对应一个hash映射的值,例如0,1等,可能会出现不同的key,但是hash映射的位置相同,例如16、32等,这采用
链表结构存储hash映射值相同的所有数据(JDK8+在单个链表长度大于阈值8时自动转换为红黑树,删除节点使某单个链表节点数小于阈值6时会自动从红黑树退化为链表结构)。

JDK1.8以前版本的实现HashMap底层采用的是Entry数组和链表实现。JDK1.8和以后的版本是采用Node数组(Entry)+ 单向链表 + 红黑树 实现。
(Java7中使用Entry来代表每个HashMap中的数据节点,Java8中使用Node,基本没有区别,都是key,value,hash和next这四个属性,不过,Node只
能用于链表的情况,红黑树的情况需要使用TreeNode。)

 Map主要用于存储键key值value对,根据键得到值,因此键不允许重复,但允许值重复。
     * HashMap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
     * HashMap最多只允许一条记录的键为null;value允许多条记录的值为null
     * HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致,在JDK1.7中
     * 会出现环形链( 如何判断环型链?创建一个Set,然后遍历整个集合,将每个元素的key值存入set,如果要存的key值已经存储在set中,则出现环型链),
     *虽然JDK1.8不会出现环形链,但是还会有rehash操作出现死循环、脏读问题、size值不准确等问题。
     * 如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力。


HashMap的put方法的具体流程:
public V put(K key, V value){
return putVal(hash(key), key, value, false, true);
}
真正存储键值对的实现是在put方法中调用了内部的putVal方法,具体的操作都在这个方法中。
参数1是hash值(先将要存储的key值传入hash方法中获取一个hash值),参数2是要存储的key值,参数3是要存储的value,参数4表示如果当前位置已存在一个值
是否替换,false是替换,true是不替换。参数5是否在创建模式,如果为false,则表示在创建模式。

hash方法将将高位数据移位到低位进行异或计算,这是因为有些数据计算出的哈希值差异在于高位,而HashMap里的哈希寻址是忽略容量
    以上的高位的,可以有效地避免类似情况下的哈希碰撞
     static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
进入putVal方法
1、如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容。(resize方法
         * 兼顾两个职责,创建初始化数组[因为数组采用的是延迟处理]或者在容量不满足需求时进
         * 行扩容处理。插入数据if (++size > threshold)resize();进行条件扩容
2、计算插入存储的数组索引i,具体的索引下标值为插入数据的key算出来的hash值对数组长度求余
         * 如果对应位置上没有数据则直接新创建node对象插入到对应的桶上

3、    插入key对应的value,首先根据key计算对应的桶位置下标(n - 1) & hash,然后再判断桶上是否存储了数
            据。如果没有存储数据则将key和value封装为Node对象【单向链的第一个节点】,插入对应的桶位置;如果
            已经插入了数据【对应位置上应该有一个单向链或者红黑树,如果第一个节点为Node则单向链,如果TreeNode
            则红黑树】,则根据是否为TreeNode,进行进一步处理,如果是红黑树,则调用TreeNode中的putTreeVal方
            法将数据插入红黑树中;如果是单向链表,遍历当前桶位置上的整个链表,如果某个节点上的key值和插入的key
            相等则后盖前【key不变value变】,如果所有的节点上的key值和插入的key值不相等,则插入到当前链表的末尾
            ,最后再判断是否需要进行树化处理
4、在putVal()中看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时
    会对其进行扩容,或者当该数组的实际大小大于其临界值,这个时候在扩容的同时也会伴随的桶上面的元素
    进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据
    Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为
    0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位
    置上。
总结
简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值