HashMap的详细介绍(2)

HashMap的详细介绍

—–本文只针对1.7版本的HashMap所讲解.

通过上一章的讲解HashMap的详细介绍(1)我们知道了HashMap的构造过程,随即引出了两个问题,一:HashMap的容量为什么一定要是2的次幂?二:如果put元素数量大于阈值 一定会扩容吗?现在我们就来对这俩个问题进行讲解.看代码:

    public V put(K key, V value) {
        //这里初始化数组,上节课说过了,不清楚的同学请看上节课的介绍
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //判断我们传入的key是否为null,因为我们key不为null,这里我们先不讲解,后面随即讲解,
        if (key == null)
            return putForNullKey(value);
            //对key生成hash码
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

这里对key进行生成hash码,它是怎么生成的呢?我们看下面的代码:

    final int hash(Object k) {
        int h = hashSeed;//这里为0
        //0 !=0 条件不成立,执行后面的.
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        //这里通过k的hashCode去和h异或去二次生成hash值返回
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

hashSeed为什么为0可以参考HashMap的hashSeed的问题,然后我们执行indexFor这个方法,进入这个方法我们就能得到答案了,看代码:

    //这里传入了两个参数,一个是我们上面生成的hash码,另一个是我们的容器容量.
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

为什么说答案在这里,我们看这里的代码只有一行,看看它做了什么事情,我们默认的长度为16,假设我们的哈希值为100,100的二进制位1100100,16-1为15,15为1111,我们最后得到 1100100 & 1111
最后结果为100,我们在模拟一下,假设传入的是102,得到1100110 & 1111 最后结果为:110. 但是我们的length不是2的次幂会发生什么问题? 假设我们length为6 ,6-1 为5 5的二进制为101 ,还是上面两个数字 先看100 , 1100100 & 101 最后得到结果为 100 , 再看102 1100110 & 101 最后结果也是100 导致他们生成的数值一致,最后所生成的数组位置也形成了一致,最后形成了一个链表结构,链表结构会导致我们查询的效率变低,所以为什么一定要是2的次幂,相信在这里大家都已经知道了,一个就是更好的生成存储位置,更好的解决位置冲突,从而提高查询效率,一个数组的位置,尽量放置一个Entry(也就是put进去的元素).这样查询更快,而不用遍历链表。我们继续往后面看:

      public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        //这里执行了indexFor返回了一个索引赋值给i变量
        int i = indexFor(hash, table.length);
        //table[i] 就是HashMap的数组,也就是拿到数组[i]的位置赋值给e,e如果不为null,则进入,如果没有,则跳出
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //因为我们进去了 肯定e不为null
            //所以判断,这次put的元素hash是否等于当前位置的hash值是否一致,如果一致在比较key是否相等,如果相等,那么条件满足,进行覆盖操作,因为我们知道HashMap的key是唯一的,如果key相等,那么执行覆盖
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                //把以前的value赋值给oldValue 
                V oldValue = e.value;
                //把这次put的value赋值给e,形成覆盖
                e.value = value;
                //这里是一个空方法,不需要关注
                e.recordAccess(this);
                //我们把以前的value返回,所以当我们去覆盖一个元素的时候,你可以拿一个变量去接收,它可以返回旧值.
                return oldValue;
            }
        }

        //这个是记录我们对HashMap操作的次数,但是我们可以看见,
        //在1.7的时候,如果我们只是修改数据,它不会执行到这里,这里也就不能自增
        modCount++;
        //这里就是添加新的元素了,看看下面代码,我们就可以解决第二个问题了
        addEntry(hash, key, value, i);
        return null;
    }

上面第一个问题已经解决,第二个问题,如果put元素数量大于阈值 一定会扩容吗?
答案是不一定(1.8是会扩容的!),看代码:

    void addEntry(int hash, K key, V value, int bucketIndex) {

        if ((size >= threshold) && (null != table[bucketIndex])) {
            //扩容,怎么扩容的 ,同学们好好思考一下?
            resize(2 * table.length);
            //扩容后,会再次重新计算我们的hash值
            hash = (null != key) ? hash(key) : 0;
            //在重新计算我们的位置
            bucketIndex = indexFor(hash, table.length);
        }
        //这里是真正添加元素的地方,这里传入了4个参数,它们都和这次put进去的元素,是息息相关的
        //参数1:它的hash值,参数2:它的key,参数3:它的value,参数4:它生成的位置
        createEntry(hash, key, value, bucketIndex);
    }

当我们添加元素的时候,它会比较size是否大于阀值,如果大于它还会去比较你生成位置是否为null,看这里如果我们的生成的位置如果不为null 就算大于阈值也不会去扩容的。所以我们这里第二个问题也随即解决。然后我们再来看看它是怎么添加元素的,看代码:

    //参数1:它的hash值,参数2:它的key,参数3:它的value,参数4:它生成的位置
     void createEntry(int hash, K key, V value, int bucketIndex) {
         //这里把当前位置元素(null)赋值给e
        Entry<K,V> e = table[bucketIndex];
        //看我们这里new了一个Entry对象,这里就创建了一个新的对象
        //给当前生成的位置table[bucketIndex],这里就添加成功
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //纪录元素的个数
        size++;
    }

同学们想一想如果e不为null会是什么情况?这里应该能看出来的,同学们好好想一想,上面的代码应该能看出来。还有我上面跳过的问题,当key为null时,它又会怎么操作?好了,这一章我们就到这里,下期再见.


书山有路勤为径,学海无涯苦作舟。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值