引入:已经有了TreeMap,为什么还要有HashMap?
如果数据比较少,使用TreeMap存储,红黑树还需要旋转、变色等操作,有点大材小用;
数据量少点情况下,直接使用数组最为简单;随着时间的推移,存储的数据越来越多,数组达到瓶颈;
如果直接使用红黑树,数据量越大,所需要的旋转、变色也就越多;可见,没有一种数据结构能满足不同状态的所有需求
需要选择多种数据结构来满足需求。
HashMap结构
数组+链表+红黑树(默认空间为16)
HashMap 的内部结构是一个数组,这个数组的每一个元素都是一个链表或树(在 Java 8 之后,链表长度超过一定阈值时会转为红黑树)。每个键值对存储在数组的某个位置(即桶位)中。如果多个键的哈希值对应相同的桶位,那么这些键值对会被放在同一个链表或树中。
初始状态:
数组:HashMap的底层是一个数组(即哈希表),每个数组元素是一个桶,这些桶刚开始都是空的。(主体)
链表:每一个桶其实是一个链表的头节点(或数根节点),一开始桶内没有元素时,链表也是空的。(为了解决hash冲突)
存储结构:
存储元素时:
元素存储:当你向HashMap中插入一个元素时,HashMap会根据键的hashCode()计算该元素应该存放在哪个桶(数组的索引位置)中。
链表初始化:如果这个桶是空的(即这个位置上没有任何元素),HashMap 会在这个位置创建一个新的链表节点,并将元素存储在该链表节点中。
链表存储:如果该桶已经有元素(链表节点)了,会进行key. equal进行判断,key值相等则进行替换,不相等则会以链表的形式追加到这个链表的末尾。
hash算法
在进行数据插入时(默认大小为16),需要获取一个0-15的索引,获取索引之后,将数据按照索引的位置插入,索引该如何获取???
后续还需要使用索引从中得到数据,所以每一个数据需要保证同一个key每次生成的索引值是相同的
方法一(jdk1.8之前):对插入的key进行hash计算之后对16进行取余(效率较低)
方法二:使用hash&(16-1)(哈希碰撞较高)
方法三:使用扰动函数减少哈希冲突,高16位和低16为进行异或运算之后得到新的hash值,在与n-1进行&运算
在计算机科学,尤其是在哈希表的实现中,“扰动函数”(perturbation function)通常是用来进一步混淆哈希值,以减少哈希冲突的可能性,确保哈希值在数组中的分布更加均匀。
作用和目的:
1. 减少冲突:在哈希表中,如果两个不同的键被哈希到相同的索引位置,就会发生哈希冲突。虽然好的哈希函数可以尽量避免冲突,但在实际应用中难免会有冲突的情况。扰动函数通过对初始哈希值进行进一步的扰动,减少了哈希冲突的发生。
2. 分布均匀:扰动函数有助于将相似的哈希值分布到不同的桶(数组索引)中,确保哈希表的数据分布更为均匀,从而提高哈希表的查找效率。
扰动函数是哈希表中的一种技术手段,通过进一步处理哈希值,减少冲突并提高哈希表中数据分布的均匀性。虽然它不是所有哈希表实现中的必备部分,但在一些对性能要求高的哈希表实现中(如 HashMap),它起到了重要的优化作用。
HashMap扩容
扩容因子:0.75
也就是说当元素个数达到数组的3/4,也就是9个的时候,数组会进行扩容,扩容为当前的一倍32
数据迁移:在扩容之后,需要对原来链表中的数据进行数据迁移,所有数据需要重新进行哈希计算,并迁移到新的数组中。
为什么数组长度扩容需要为2的幂次方?
在进行hash计算的时候,使用数组长度-1的二进制值进行&运算,若数组长度-1为偶数,则会出现问题
在HashMap中,虽然可以设置初始化值,但是HashMap并不会按照设置的值作为初始值,而是会使用设置值的最接近的2的幂次方作为初始值。
红黑树转换
在数组长度达到一定长度,使用扩容不再能够达到解决链表增加的情况时。也就是在数组长度大于等于64不能够进行扩容,链表长度大于等于8,将链表转换成对应的红黑树;在红黑树的节点小于6的情况下(否则在临界点增删元素时,会导致红黑树和链表之间来回切换),红黑树会转变为链表
红黑树详解
HashMap源码解析
插入数据时计算key所对应的hash值
hash(key):获取key的哈希值,并将所得到的哈希值进行无符号右移16位,与哈希值进行异或运算得到新的哈希值
key.hashCode():被标记为 @IntrinsicCandidate 的方法可能会在 JVM 内部使用专门的优化或实现,直接在本地代码(如 C++ 或汇编)中执行,而不是通过 Java 方法调用的常规机制。
Node:
putVal:
resize():