HashMap源码分析之put和get

今天我来和大家梳理下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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值