HashMap源码阅读02

1、前言

上篇文章《HashMap源码阅读01》中记录了一个问题,当HashMap依次put键值对时,并没有按照由小到大的顺序排列,而是跳过了中间几个数字,今天我们就来一探究竟:
在这里插入图片描述

2、解答

我们的测试代码如下:

    @Test
    public void test(){
        Map<String,Object> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
        map.put("d",4);
        map.put("Ed",4);
//        map.put("E",4);
        map.put("F",4);
        map.put("G",4);
        map.put("hd",4);
//        map.put("h",4);
        map.put("I",4);
        map.put("J",4);
        map.put("K",4);
        map.put("l",4);
        map.put("M",4);
        Integer i = (Integer) map.get("a");
        System.out.println(i);
    }

当map放入(“Ed”,4)这个键值对时,我们通过debug发现,在进行(n - 1) & hash运算时,得到的结果是15,所以该键值对被放入了索引15的位置上。
当map放入(“hd”,4)这个键值对时,(n - 1) & hash运算结果是12,所以该键值对被放入了索引12的位置上。

经过debug调试,我们得知一条规律,在HashMap调用put方法放入键值对时,是通过(n - 1) & hash的与运算来确定该键值对将放入的索引位置。
我们通过改变测试案例,进一步调试来验证这条规律。比如将放入键值对的顺序打乱后,最终链表各键值对得到的位置排列依旧不变。

我们再来看看(n - 1) & hash这个与运算表达式,里面有两个变量:
n:当前链表的长度
hash:要进行put操作的键值对中的key的hash值
所以,当链表长度不变得前提下,进行put操作得各键值对是依据各自key的hash值来进行链表位置的散列分布。
以上结论,即可解答文章开头提出的问题。

3、提问

那么当HashMap多次放入key相同的键值对导致(n - 1) & hash运算的结果相同的情况下,会发生什么呢?

4、测试与分析

我们一起来看下:
首先,修改我们的测试案例:

        map.put("a",1);
        map.put("a",11);

让map同时put(“a”,1)与(“a”,11)两个键值对操作,这样就造成了最终(n - 1) & hash运算结果相同,我们进入putVal方法:
当进行判断时,(p = tab[i = (n - 1) & hash]) 不为空,将执行如下代码:

            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

我们分析下这段代码做了什么:
首先,定义了两个变量:

            Node<K,V> e; K k;

进入判断:

            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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;
                }
            }

这里判断成立的条件是:
(1)对应位置的节点的hash属性与目标键值对的key的hash值相等
(2)对应位置的节点的key属性与目标键值对的key相同
①如果判断成立:则将链表上目标节点的节点对象赋值给变量e.
②如果判断不成立:也就是说,虽然原键值对的key的hash值与目标键值对的key的hash值相同,但key本身却并不一样。
这里会判断原键值对对象的类型是否属于TreeNode:
如果判断成立,则调用putTreeVal方法。(此方法做了什么,我们另外说明)
如果判断不成立,则进入一个for循环,从原节点开始,沿着链表往末端遍历节点,这里有两种情况可以跳出for循环:
1、满足上述(1)(2)的判断条件
2、当前节点无下一节点,即为链表末端

我们先看第一种情况,当找到满足上述(1)(2)的判断条件的节点时,跳出for循环后执行下面的代码:

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

当允许替换value或者旧值为空时,可以用新value替换掉旧value.
在HashMap中afterNodeAccess方法实现为空:

    void afterNodeAccess(Node<K,V> p) { }

将旧值返回,方法结束。
接下来我们看第二种情况,当前节点无下一节点,即到达链表末端后,执行以下代码:

                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

新构建一个节点对象放在链表末端。(还进行了一个判断,此处不讲,另作说明)

5、结论

综合以上所述,我们得知,HashMap调用put方法发生hash冲突时,HashMap如何解决与处理的。
首先,HashMap会判断key是否相同,如果是同一个key,则进行value替换操作;如果不是同一个key,但原链表节点是TreeNode类型时,会调用putTreeVal方法;如果不是同一个key,而且原链表节点不是TreeNode类型时,HashMap会从hash冲突节点开始,沿着链表寻找是否存在key值与待放入键值对的key相同的节点,如果有,则进行值替换操作,如果没有,则在链表末端添加一个由待放入键值对构建的新节点。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值