今天我来和大家梳理下HashMap的核心两个方法:put和get方法,里面会涉及到以下几个问题:
1. hashMap使用到的数据结构(数组、链表、红黑树)
2. 数组的扩容规则、链表转换成红黑树的条件以及红黑树什么情况下又会转成链表
3. hashMap存储元素时,hash冲突时如何具体解决
带着上述三个问题,我们可以在put和get方法中找到相应的答案
一:整体架构
HashMap底层的数据结构主要是:数组+链表+红黑树,其中当链表的长度大于等于8时,链表会转换成红黑树,当红黑树的大小小于等于6时,红黑树会转化成聊表,整体的数据结构如下:
图中左边竖着的是HashMap的数组结构,数组的元素可能是单个Node,也可能是个链表,也可能是个红黑树,例如数组下标索引为2的位置就是一个链表,下标索引为10的位置对应的就是红黑树。
好,我们接着看下HashMap的类注释,可以得到如下信息:
l 允许null值,不同于HashTable,是线程不安全的;
l Load factor(影响因子)默认值是0.75,是均衡了时间和空间损耗算出来的值,较高的值会减少空间开销(扩容减少,数组大小增长速度变慢),但增加了查找成本(hash冲突增加,链表程度变长),不扩容的条件:数组容量 > 需要的数组大小 /load factor;
l 如果有很多数据需要存储到HashMap中,建议HashMap的容量一开始就设置成足够的大小,这样可以防止在其过程中不断的扩容,影响性能;
l HashMap是非线程安全的,我们可以自己在外部加锁,或者通过Collections#synchronizedMap来实现线程安全,Collections#synchronizedMap的实现是在每个方法上加上了synchronized锁;
l 在迭代过程中,如果HashMap的结构被修改,会快速失败。
常见属性:
二:put方法流程
新增key,value大概步骤如下:
1. 空数组有无初始化,没有的话进行初始化
2. 如果通过key的hash进行(n-1)&hash计算数组下标索引i的对象没有元素,则存储到相应索引为i的数组中去,否则即为发生了hash冲突,转到3
3. 发生hash冲突有种特殊情况,key相同,会根据onlyIfAbsent的值来覆盖value值。排查这种情况,hash冲突,有两种解决方案:链表or红黑树
4. 如果是链表,递归循环,把新元素追加到链表的队尾
5. 如果是红黑树,调用红黑树的新增方法
6. 判断是否需要扩容,例如数组元素为16,put的次数大于16*0.75=12时,即会进行扩容,新的数组大小为32,扩容临界值为32*0.75=24。
具体的流程示意图如下:
代码细节如下:
1 /**
2 * Implements Map.put and related methods
3 *
4 * @param hash hash for key
5 * @param key the key
6 * @param value the value to put
7 * @param onlyIfAbsent if true, don't change existing value
8 * @param evict if false, the table is in creation mode.
9 * @return previous value, or null if none
10 */
11//入参 hash:通过hash算法计算出来的值
12//入参 onlyIfAbsent: false表示即使key已经存在,任然会用新值来覆盖原来的值,默认为false
13final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
14 boolean evict) {
15 //n表示数组的长度,i为数组索引下标,p为i下标位置的node值
16 Node<K,V>[] tab; Node<K,V> p; int n, i;
17 //如果数组为空,使用resize()方法初始化
18 if ((tab = table) == null || (n = tab.length) == 0)
19 n = (tab = resize()).length;
20 //如果当前索引位置为空的,直接生成新的节点在当前索引位置上
21 if ((p = tab[i = (n - 1) & hash]) == null)
22 tab[i] = newNode(hash, key, value, null);
23 //如果当前索引位置有值的处理方法,即解决hash冲突问题
24 else {
25 //当前节点的临时变量
26 Node<K,V> e; K k;
27 //如果key的hash和值都相等,直接把当前下标位置的node值赋值给临时变量
28 if (p.hash == hash &&
29 ((k = p.key) == key || (key != null && key.equals(k))))
30 e = p;
31 //如果是红黑树,使用红黑树的方式新增
32 else if (p instanceof TreeNode)
33 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
34 //是个链表,把新节点放到链表的尾端
35 else {
36 //自旋
37 for (int binCount = 0; ; ++binCount) {
38 //e = p.next表示从头开始,遍历链表
39 //p.next == null表明p是链表的尾节点
40 if ((e = p.next) == null) {
41 //把新节点放到链表的尾部
42 p.next = newNode(hash, key, value, null);
43 //当链表的长度大于等于8时,链表转红黑树
44 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
45 treeifyBin(tab, hash);
46 break;
47 }
48 //链表遍历过程中,发现有元素和新增的元素值相等,结束循环
49 if (e.hash == hash &&
50 ((k = e.key) == key || (key != null && key.equals(k))))
51 break;
52 //更改循环的当前元素,使p在遍历过程中,一直往后移动
53 p = e;
54 }
55 }
56 //说明新节点的新增位置已经找到了
57 if (e != null) { // existing mapping for key
58 V oldValue = e.value;
59 //当onlyIfAbsent为false时,才会覆盖值
60 if (!onlyIfAbsent || oldValue == null)
61 e.value = value;
62 afterNodeAccess(e);
63 return oldValue;
64 }
65 }
66 //记录hashMap的数据结构发生了变化
67 ++modCount;
68 //如果hashmap的实际大小大于扩容的门槛,开始扩容
69 if (++size > threshold)
70 resize();
71 afterNodeInsertion(evict);
72 return null;
73}
链表的新增:链表的新增比较简单,就是把当前节点追加到链表的尾部。当链表的长度大于等于8,并且整个数组大小大于64时,才会转成红黑树,当数组小于64时,只会触发扩容,转化成红黑树的过程可以参照(https://github.coom/luanqiu/java8,目前这块我自己也还没弄明白)思考:为什么选择8,大于8的时候,才进行转换成红黑树
红黑树新增节点过程:还没搞明白,后期补上。
三:查找get过程
HashMap的查找主要分以下几步:
1. 根据(n-1)&hash得出索引的位置,且该数组位置上对应的元素值不为空,否则返回null
2. 获取当前i位置的hash值和要查找的hash(key对应的)以及key值是否相等,是的话,直接返回,不是的话往下走
3. 判断当前节点有无next节点,有的话判断是链表类型还是红黑树类型
4. 分别走链表和红黑树不同类型的查找方法
链表查找的关键代码是:
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//根据(n-1)&hash得出索引的位置,且该数组位置上对应的元素值不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//hash和key都相等,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//红黑树的查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//链表的查找,hash和key相等,则表明找到
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
好了,hashMap的put和get方法流程就讲完了,涉及红黑树的部分,没有讲解,等进一步详细的了解再在后面补充。HashMap核心的知识点就是:数据结构、hash冲突解决、扩容,这些从上面都可以找到答案。
参考资料:https://www.imooc.com/read/47/article/850?distId=7f8302&utm_source=fenxiao