hashMap的底层机制和源码
1、hashMap的底层示意图
hashMap的底层实际上是数组+链表的形式,这个数组的一个值就是实现了Map
E
n
t
r
y
的接口
H
a
s
h
M
a
p
Entry的接口HashMap
Entry的接口HashMapNode, 通过源码知道,Entry是Map里边的一个内部接口,而HashMap
N
o
d
e
是
H
a
s
h
M
a
p
里边的一个静态内部类,而且实现了
M
a
p
Node是HashMap里边的一个静态内部类,而且实现了Map
Node是HashMap里边的一个静态内部类,而且实现了MapEntry接口
2、hashMap的扩容机制
- HashMap底层维护了个Node类型的table,默认为null;
- 当创建对象时,将加载因子(loadfactor)初始化为0.75 , 这个是用来干嘛的呢?用来作为触发扩容机制的条件,比如当前数组大小为16,当加入的数据个数到达了 16* 0.75 = 12 的时候,就会对当前的容量进行扩容。
- 当添加key-val时,通过key的哈希值来得到在table中的索引位置,然后判断该索引处是否存在元素,如果没有元素就直接添加,如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val; 如果不相等需要判断是树结构还是链表结构,如果是链表结构,依次遍历链表,看看有没有相同的key,有相同的key的话就将val进行替换,否则就把节点加入到链表后边,当然还要考虑到链表是否需要转化成树结构;如果是树形结构,也要进行相应处理,下边我会说到。
- 第一次添加,就需要扩容table容量为16,临界值(threshold)为12,这个也就是触发扩容机制的临界值。
- 以后在扩容,则需要扩容table容量为原来的2倍,当然,临界值也变成了原来的两倍,即24,依次类推。
- 在java8中,如果一条链表的元素超过 TREEIFY_THRESHOLD(默认是8),并且table的大小>= MIN_TREEIFY_CAPACITY(默认64),就会转化为树形结构(红黑树)
3、hashMap的源码分析
首先我们写一段简单的hashMap的存值的代码
public class MapResource {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java", 10);
map.put("php", 10);
map.put("java", 20);
System.out.println(map);
}
}
当然输出结果为:{java=20, php=10}
我们进行一个debug
在执行hashMap的构造方法时候就初始化了加载因子(loadFactor = 0.75)
然后开始执行put方法
这里边有一个hash(key)的方法,主要就是用来计算key的hash值,然后通过这个hash值来定位到在table表上的位置,这个比较的好理解。
这里我们可以进入hash(key)的方法看一下实现,反正就是返回一个int类型的hash值。
然后进入putVal()的方法:
这个方法就是hashMap存值的核心代码,看着那么多ifelse 脑壳有点疼,我们可以一步一步来分析。
首先:
Node<K,V>[] tab; Node<K,V> p; int n, i; //这个就是定义一些辅助变量,这个没什么好说的
我们第一次加数据的时候 ,很显然我们的table表是null的,而且长度是0, 因此会进入到这个if分支,主要会执行resize()这个方法,这个方法其实就是初始化table:
if ((tab = table) == null || (n = tab.length) == 0) //table表为null,并且长度为0
n = (tab = resize()).length;
我们可以进入到resize()这个方法中去看看
我们可以看到 这个table数组中每个元素都是一个Node<K,V> 结构的数据,然后进行下一步
//这一步是用来判断通过这个hash值获取索引,通过索引在table表上找到具体的位置,然后比较这个位置是不是为空的,当然,我们这里存值第一次进来,首先初始化table表大小后,显然所有的位置都是null,自然会进入这个分支。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
可以很清楚的看到,就是新创建一个Node节点节点挂在tab[i] 下,至于这个newNode()方法我们也可以进去看一下。
实际上就是初始化了一个Node节点并返回,Node节点其实就是一个链表的数据结构:
第一次加入数据的时候,table表什么都没有,那么就很顺利的将值放入table中,然后继续看源码,可以知道,代码执行到
++modCount; //修改次数+1
if (++size > threshold) //判断是否需要扩容,看看是否大于临界值,显然这一次size才1,不会触发扩容机制
resize();
afterNodeInsertion(evict);
return null;
然后第一次的存值就完成了。
然后进行第二次的存值,第二次我们的key是php,通过计算,hash值显然和java不一样,那么就会进入到
//同样的,计算后的hash值在table表上找到的索引位置不一样,而且当前位置没有元素,为null,那么就创建一个新的节点挂在上边。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
接下来就是就是执行下边这段代码了。
++modCount; //修改次数+1
if (++size > threshold) //判断是否需要扩容,看看是否大于临界值,显然这一次size才1,不会触发扩容机制
resize();
afterNodeInsertion(evict);
return null;
然后我们进行第三次存值,第三次我们存入的key是java,和第一次一样,那么计算得到的hash值就是一样的,肯定在table表上的索引位置就是一样的,此时我们来看源码。
当出现hash冲突的时候,就会走到这个分支下,而且有三种情况,我们来具体分析一下
Node<K,V> e; K k; //这个也是辅助变量,没什么好说的
第一种情况:
//能够进入这里的原因就是和之前第一次添加的java的hash值一样,并且是同一个对象或者你的key的equals方法一样,那么就将e指向p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
第二种情况:
//就是说,如果这个table表当前索引位置的数据的数据结构是树机构,那么用树的处理方法来解决,通过这个putTreeVal方法来处理。具体里边干了什么,就不展开细说了。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
第三种情况:
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
一进来就是一个for循环,还没有循环次数条件,是一个死循环,就是说找到了,但是这个节点后边挂着的是链表,新加入的这个节点就会去比对这个链表的每一个节点。
- 如果在比对的过程中找到了和新加入的节点的key相同的节点,也就是循环到了这段代码:
//这个判断条件是不是很眼熟,就是和第一种情况的一样。在比对节点的时候发现新加入的节点的key和比对的节点的key一样,那么就结束循环,我们可以发现,在上次循环的时候,p已经指向了e了,也就是说,在结束循环的时候,已经找到了需要处理的节点在链表中的位置。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
- 还有一种情况,如果节点从头到尾比对完了还没有找到怎么办呢?这个时候就会走到这段代码
if ((e = p.next) == null) { //相当于就是在末尾了,p.next == null了
p.next = newNode(hash, key, value, null); //这段就很眼熟了,就是在链表后边新加入一个节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
加节点也不可能一直在链表的后边加节点,前面说到过,当节点的长度到达8的时候,就会进行树化,于是会执行下边这个方法
//加入后,判断当前链表的个数,是否已经到8个,到8个后,就调用treeifyBin 方法进行红黑树的转化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st TREEIFY_THRESHOLD = 8
treeifyBin(tab, hash);
但是调用这个方法一定就进行树化吗?其实还有条件才能进行树化,我们可以点开treeifyBin() 方法进行查看
也就是说,如果当前table表为null或者table表的长度小于64,那他只会执行resize() 方法对table表进行扩容,而不会马上树化,综上所述,只有当某一个节点链表的长度大于8了,并且table表的长度大于64了,那么这个链表才会转化为红黑树。
最后,不管你是树化也好,还是找到新的节点也好,在循环中都会将p指向e
然后跳出循环以后,就会来到下面这串代码
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
这段代码我们可以清楚的看到,就是替换值的一个过程。
上边就是HashMap存值的全部过程了。
在这里打个小广告,欢迎访问我的个人博客:https://www.xiaoger.top