这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天
今天让我们走进hashMap是如何进行实现的,查看其实现原理。
了解HashMap
哈希表中进行添加删除查找操作性能十分高。在没有哈希冲突的情况下仅需要一次就可以完成查询。
Map之间的关系是映射关系。比如我们要新增或查找某个元素。我们通过把当前关键词(key)通过映射计算找到直接找到相应的数据位置。
而通过关键词来计算出数据位置的函数就叫做哈希函数。这个函数的好坏会直接影响到哈希表的优劣。
查找操作就是通过关键词计算出实际存储地址,然后取出相应数据。
哈希冲突
但是存在一个问题。有可能不同的关键词通过哈希计算出实际的存储地址是相同的。这时候就出现了哈希冲突,在进行插入操作的时候,发现已经被其他元素占用了。这也叫做哈希碰撞。好的哈希函数会尽量的保证计算简单并且地址分布均匀。但是再好的哈希函数都无法避免地址发生冲突。
如果发生冲突了使用链地址法。使用数组+链表的方式来实现在同一地址放入多个数据。但是对于后面还有更多的优化。
HashMap源码
让我们来看一下Java中HashMap的源码
HashMap的初始化
在文档中清晰的写出构造一个空hashMap
初始化空间为16
,负载因子为0.75
。
负载因子
什么是负载因子,实际上就是已经占用数组的地址与数组地址的比值。也就是已经使用地址,占有所有地址的比。因为在出现碰撞后,会进行链地址操作将相同的地址数据。如果这个比值较大就说明地址数组快满了,如果不进行扩容后面就可能会引起大量的hash碰撞从而影响性能。
负载因子为什么是0.75,在hash计算中结果呈现为泊松分布
,在负载因子为0.75的时候链表长度的可能超过8的概率就很小了。如果使用更小的负载因子可能会导致更多的空间浪费,而更大的负载因子又会增加hash碰撞的概率性能就会下降。
在hashMap源码文档中也详细说明了,在负载因子为0.75时,层数大于8的概率小于百万分之一。
Map接口
map接口的属性包括方法和内部类
其中内部类Entry接口就包含了作为Map的每个单元所要实现的方法
Entry
实际上就是Map中的每个存储单元,接口包括对该单元的获取key,value方法以及对值的比较方法。
HashMap对Map方法的实现
Entry类
在HashMap中Node
是每一个存储单元, Node
对象实现了Entry
接口的方法,这单个元素就可以完成对数据的更新和取值。
put方法
让我们看一下,HashMap的put方法是如何实现的。
put方法的逻辑实现实际上是由putVal来实现的,在调用putVal时会传入key的hash值。使用的方法是hash。
从hash方法中我们可以看出在key为
null
时hash的取值为为0,从中我们也可以延申出一个问题。
null
能不能作为hashMap的key答案:可以,key为null是可以计算出hash值的此时hash值为0。可以正常存入到hashMap中去。
put方法的过程:
- 首先判断map是否为空,如果为空先执行
resize
方法对空间进行初始化 - 通过hash判断此存储位置是否有数据,如果没有则进行存储
- 如果有对象,首先对此对象进行
equals
判断,如果相等说明key已经存在只需要对值进行更新即可。 - 如果hash相同key不同,就需要进行存储,在存储前会判断在此地址存储对象的数量如果,在此地址存储数量超过8就不再使用链表进行存储而是换为红黑树结构。
- 在存储完成后会对
size
进行加一操作,如果大小大于存储容量*负载因子
会执行resize
方法对空间进行扩容。
resize方法
resize我们在外部无法直接访问,resize会有threshold变量来表示容量大小,MAXMUM_CAPACITY表示最大容量
resize方法执行过程:
- 在进入方法后首先会判断当前的map大小否为0如果为0会按照默认的空间大小进行初始化。
- 如果大小不为0先判断扩容大小是否大于MAXMUM_CAPACITY,MAXMUM_CAPACITY大小为
1<<30
,如果大于就直接使用Integer的最大值作为新扩容的值,如果不大于最大空间则会执行oldThr<<1
来使空间扩容一倍。 - 按照扩容大小新建table对象然后对就table对象进行拷贝。
remove方法
rome方法就是put方法的逆向操作。和put的流程相似。
put方法的执行过程:
- 首先先计算hash值看是否存在值并且进行
equal
操作,如果hash和equal后发现相等就执行移除操作。 - 如果
hash
相等,equals
发现不相等,这种存储情况就是链表或者红黑树存储,那就要进行遍历查询。找到要删除的元素。 - 最后对map的数量进行减一操作。