深入理解HashMap | 青训营笔记

33 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天

今天让我们走进hashMap是如何进行实现的,查看其实现原理。

了解HashMap

哈希表中进行添加删除查找操作性能十分高。在没有哈希冲突的情况下仅需要一次就可以完成查询。

Map之间的关系是映射关系。比如我们要新增或查找某个元素。我们通过把当前关键词(key)通过映射计算找到直接找到相应的数据位置。

而通过关键词来计算出数据位置的函数就叫做哈希函数。这个函数的好坏会直接影响到哈希表的优劣。

image.png 查找操作就是通过关键词计算出实际存储地址,然后取出相应数据。

哈希冲突

但是存在一个问题。有可能不同的关键词通过哈希计算出实际的存储地址是相同的。这时候就出现了哈希冲突,在进行插入操作的时候,发现已经被其他元素占用了。这也叫做哈希碰撞。好的哈希函数会尽量的保证计算简单并且地址分布均匀。但是再好的哈希函数都无法避免地址发生冲突。

如果发生冲突了使用链地址法。使用数组+链表的方式来实现在同一地址放入多个数据。但是对于后面还有更多的优化。

image.png

HashMap源码

让我们来看一下Java中HashMap的源码

HashMap的初始化 image.png 在文档中清晰的写出构造一个空hashMap初始化空间为16负载因子为0.75

负载因子

什么是负载因子,实际上就是已经占用数组的地址与数组地址的比值。也就是已经使用地址,占有所有地址的比。因为在出现碰撞后,会进行链地址操作将相同的地址数据。如果这个比值较大就说明地址数组快满了,如果不进行扩容后面就可能会引起大量的hash碰撞从而影响性能。

负载因子为什么是0.75,在hash计算中结果呈现为泊松分布,在负载因子为0.75的时候链表长度的可能超过8的概率就很小了。如果使用更小的负载因子可能会导致更多的空间浪费,而更大的负载因子又会增加hash碰撞的概率性能就会下降。

在hashMap源码文档中也详细说明了,在负载因子为0.75时,层数大于8的概率小于百万分之一。

image.png

Map接口

map接口的属性包括方法和内部类

image.png 其中内部类Entry接口就包含了作为Map的每个单元所要实现的方法

image.png Entry实际上就是Map中的每个存储单元,接口包括对该单元的获取key,value方法以及对值的比较方法。

HashMap对Map方法的实现

Entry类

在HashMap中Node是每一个存储单元, Node对象实现了Entry接口的方法,这单个元素就可以完成对数据的更新和取值。

image.png

put方法

让我们看一下,HashMap的put方法是如何实现的。 image.png put方法的逻辑实现实际上是由putVal来实现的,在调用putVal时会传入key的hash值。使用的方法是hash。

image.png 从hash方法中我们可以看出在key为null时hash的取值为为0,从中我们也可以延申出一个问题。

null 能不能作为hashMap的key

答案:可以,key为null是可以计算出hash值的此时hash值为0。可以正常存入到hashMap中去。

put方法的过程:

  1. 首先判断map是否为空,如果为空先执行resize方法对空间进行初始化
  2. 通过hash判断此存储位置是否有数据,如果没有则进行存储
  3. 如果有对象,首先对此对象进行equals判断,如果相等说明key已经存在只需要对值进行更新即可。
  4. 如果hash相同key不同,就需要进行存储,在存储前会判断在此地址存储对象的数量如果,在此地址存储数量超过8就不再使用链表进行存储而是换为红黑树结构。
  5. 在存储完成后会对size进行加一操作,如果大小大于存储容量*负载因子会执行resize方法对空间进行扩容。

resize方法

resize我们在外部无法直接访问,resize会有threshold变量来表示容量大小,MAXMUM_CAPACITY表示最大容量

resize方法执行过程:

  1. 在进入方法后首先会判断当前的map大小否为0如果为0会按照默认的空间大小进行初始化。
  2. 如果大小不为0先判断扩容大小是否大于MAXMUM_CAPACITY,MAXMUM_CAPACITY大小为1<<30,如果大于就直接使用Integer的最大值作为新扩容的值,如果不大于最大空间则会执行oldThr<<1来使空间扩容一倍。
  3. 按照扩容大小新建table对象然后对就table对象进行拷贝。

remove方法

rome方法就是put方法的逆向操作。和put的流程相似。

put方法的执行过程:

  1. 首先先计算hash值看是否存在值并且进行equal操作,如果hash和equal后发现相等就执行移除操作。
  2. 如果hash相等,equals发现不相等,这种存储情况就是链表或者红黑树存储,那就要进行遍历查询。找到要删除的元素。
  3. 最后对map的数量进行减一操作。