什么是HashMap
HashMap是一个可以提供O(1)时间复杂度的数据结构,由数组和链表数据结构组成。在对存入的key进行hash之后,然后用hash值在数组上确定一个位置,把value对象以Node节点形式放入到数组的链表当中。
jdk1.8之后对此做了优化,因为如果发生了数据倾斜,可能会使数组某个下标的Node链表非常长,因为链表查询起来比较慢,所以1.8之后修改了,当Node链表长度大于8时,会把该下标位置的链表数据结构修改为红黑树的结构来保证查询的速度。当数据长度小于8时,会再修改为链表。
使用场景
个人理解使用场景应该是在不需要复杂的查询,只需要一个Key对应一个Value,写入少的场景。因为像HashMap,ArrayList这种数据结构都提供了自动扩容的功能,像HashMap的负载因子是0.75,也就是当数组中75%的位置都有值以后会进行扩容。每次扩容的时候都涉及到每个数据的rehash和数组的复制,所以当写入数据量非常大的时候,会不断的进行rehash和复制,有可能会造成CPU占用率非常高(这只是个人平时学习的理解,如果有不对之处请大家指正)。
所以个人感觉HashMap的使用场景也是读多写少的场景,可以提供很快速度的读,写入的速度也可以,但如果提前知道Map里要放入多少数据,最好在new对象的时候,就手动指定出长度,这样可以避免rehash,从来变相提高使用效率。
源码实现 jdk1.8版本
以下的代码都是代码在上面,解释在下面。
变量声明
|
|
这个值是HashMap默认初始化的长度,也就是16长。
|
|
这里按注释看,应该是HashMap支持的最大长度,如果超过这个值,则使用这个值。
|
|
负载因子,当数组中有值的位数超过此阀值时,进行扩容。
|
|
当数组单个位置超过此值后,会把数据结构修改为红黑树。
|
|
当小于这个值时,修改为链表。
|
|
???
|
|
存放元素的数组。
|
|
这个变量是用来存放阀值的,也就是数组长度*0.75的值。
初始化
|
|
- 初始化时,如果是空的构造方法,会只设置负载因子的值为默认的0.75.
- 如果指定的
initialCapacity
超过MAXIMUM_CAPACITY
,则值就为MAXIMUM_CAPACITY
- tableSizeFor函数会求出一个值作为HashMap的容量
|
|
- 初始化完毕
put方法
|
|
说下参数的意思,hash
就是通过key计算出的hash值,Key Value
不用多说就是我们要放入的Key和Value,onlyIfAbsent
这里我理解应该是,如果Key存在是否要替换对应Key的Value,这里传入的是false
,evict
应该是,是否自动扩容,默认是true
。
声明变量tab
数组,就是需要存放节点的数组。p
则是放在数组下标位置的节点。n,i
数组长度和位置下标。
代码逻辑
如果数组没有初始化或者长度为0,则进行初始化。resize()
方法中的初始化代码
扩容机制
|
|
|
|
这里判断现有的数组长度是否为0,如果当前数组长度大于0,并且大于或等于
MAXIMUM_CAPACITY
,则threshold
等于int的最大值,返回当前数组。newCap
的值等于当前数组的长度*2old<<1
,如果这个新值小于MAXIMUM_CAPACITY
并且当前数组的长度 >=DEFAULT_INITIAL_CAPACITY
也就是16,newThr = oldThr << 1;
这句话的意思是新的阀值=老阀值*2。其实以上的意思就是如果符合条件了,把新数组的长度和阀值都扩大2倍。
|
|
这里的意思是,如果老数组长度不大于0,并且老阀值大于0,把老阀值的值赋给新的数组长度,这里我没理解为什么要这么做。
|
|
如果以上条件都不满足,就会进行默认的初始化。也就是数组长度等于16,阀值等于12。
|
|
至此如果新的阀值还是0的话,会再次进行初始化,公式还是一样的,先根据新的数组长度*负载因子计算出阀值,如果新的数据长度小于长度最大值,并且三目表达式用来判断是否过界,如果不过界,返回计算的值,如果过界,返回int最大值。
到这里,基本上阀值和新的数组长度都已经初始化完了,创建了新的数组,开始初始化对象了。如果是构造方法里调用这里,到这里就已经结束了,因为数组,阀值,都已经初始化完毕了。
再下面的代码是扩容的时候会进行调用的。
|
|
- 到第7行为止,是表示如果老数组的下标位置只有一个节点没有链表也没有红黑树,就会把该位置的数组赋值给新数组,在新数组的位置是hash值&数组的长度(这里我以前一直认为是取余)。
- 如果是红黑树,则进行树拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
红黑树
|
|
这里我大概理解是这样的,先对所有的节点进行遍历,然后通过一定条件来判断是否小于等于UNTREEIFY_THRESHOLD
这个阀值,如果小于就切换为普通的链表,如果大于就继续在树上添加节点,由于博主对树现在理解不是很好,这里暂且这样,以后我会指定研究一下树的节构。
|
|
这里就是链表节构了,也是把链表循环先读取出来。但是这里为什么分成了2个链表?并且分别放在了不同的位置上。至此,resize()方法执行完毕,让我们再回来继续看put方法。
- 以上我们写了resize()中详细的方法
- 有初始化数组的长度和阀值
- 对数组进行扩容时对链表,单节点,以及树不同的处理
|
|
这里是数组长度对hash值求与值,如果该位置为null证明该位置没有放入元素,则放入新的元素。
|
|
这里判断,如果hash值相等并且key值相等,或者key的equest相等,就直接替换该位置的值。
|
|
如果该位置的结构是树的,则调用树的插入方法,关于树的方法博主在这里暂不讨论,因为博主也不是特别理解树结构,以后会补充。
|
|
如果是链表的话,循环所有的链表,如果有相等的key,直接替换该key对应的值,如果循环到最后也没有相等的,在链表上插入一个新的node节点,同时判断是否满足到达TREEIFY_THRESHOLD
的条件,如果到达,会把数据结构变更为红黑树。
|
|
如果到这里e已经存在了,因为e等于新放入的node节点。就把该节点移到链表最后一个,并且直接返回value值。
|
|
记录修改的次数modCount
是用来控制非法修改hashmap里的值,来抛出ConcurrentModificationException
。并且判断数组长度是否大于阀值,如果大于了就要调用resize()
进行扩容,afterNodeInsertion
没有理解作用是什么,至此put方法全部执行完毕。
get方法
|
|
put方法核心的代码是这里,这里是判断如果在数据里找到当前key的位置之后,首先判断hash值,和key是否都相等,如果都相等,直接返回此对象,如果不相等还分2种情况一种是树结构,会调用树的查找方法,另外一种是链表,则会对链表不断的循环判断,直到找到相等元素为止。
如果最后都没有找到,则返回null。
remove方法
|
|
remove方法和get方法类似,核心代码是首先也是在数组里找位置,如果当前位置首个节点是这个key,就把值给node,如果当前值不是,则进行后续查找。如果是树,调用树的查找方法,把查找到的值给node,如果是链表,则对链表进行循环查找,找到之后把值给node。
最后判断如果node不这代,并且value值相等。则会进行删除动作,如果是树节构就调用树的删除方法,如果数组的位置首节点是要删除的值,则直接把next的值给当前位置,如果是链表后续的值,则把要删除的key的下一个节点,和上一个节点连接,自己就会被删除掉。
最后增加修改次数,size减掉。返回删除的节点。