HashMap 的 源码分析

一、HashMap使用的数据结构是哈希表

        哈希表在JDK 8之前由数组和链表组成,JDK 8之后由数组、链表和红黑树组成

        我们先来简单了解下哈希表的原理,下图中有一个数组,数组元素是键值对(为了对接HashMap,哈希表不一定存储键值对,其实可以简单的理解为:存储键值对的哈希表就是HashMap),当一个键值对到来时(西瓜是键,10块是值),通过一个哈希函数,计算出键(西瓜)所对应的哈希值(为3),将该键值对存储在数组下标为3的位置,按此原理继续添加几个键值对

         一般来说,不同的键计算出来的哈希值是不相同的,但是也会有相同的情况,这种情况我们称为哈希碰撞,此时就会使用到链表将多个拥有相同哈希值的键值对连接起来

        JDK 8以前:新元素存入数组,老元素挂在新元素后面

        JDK 8以前:新元素直接挂在老元素后面

        而在JDK 8之后,为了防止链表过长,导致降低查找效率,当链表长度大于8并且数组长度大于等于64时,链表就会自动的转化为红黑树

为什么需要哈希表?

        我们想,如果上面的数组是一个水果的价格表,当我们要查找某个水果时,我们是不是只能从上到下逐一查看,才能找到我们要的那个水果的价格,此时的时间复杂度为O(n),如果通过哈希函数,直接计算出这个水果的哈希值,就直接确定了该水果在价格表中的位置,此时的时间复杂度就是O(1),效率大大提高

二、HashMap源码分析

上面讲到了HashMap的基本原理,那具体该如何通过代码实现呢?接下来就分析HashMap源码

        HashMap 有个静态内部类 Node ,在HashMap中每个元素都是一个 Node ,并且 Node 实现了 Map中的Entry接口,Node中有四个变量

  • hash:通过键计算出来的哈希值
  • key、value:键、值
  • next:用于形成链表的时候指向下一个Node

 当链表转换为红黑树后,Node也会转换为TreeNode

  • parent、left、right:分别记录父节点、左节点、右节点(prev先不用了解)
  • red:记录当前节点是红色还是黑色

并且 TreeNode 继承了 LinkedHashMap 里面的 Entry,而LinkedHashMap.Entry又继承了HashMap.Node 因此 TreeNode 内部也含有键值对

 在HashMap中有一个成员变量table,是一个Node数组,对应的就是上面讲的那个数组,

 接下来看HashMap中定义的几个常量

  • DEFAULT_INITIAL_CAPACITY:table的初始长度,1 << 4 , 1左移4位,就是16
  • MAXIMUM_CAPACITY:table的最大长度
  • DEFAULT_LOAD_FACTOR:扩容因子,0.75 表示当table的使用量达到 3/4 时,就进行扩容,扩容到之前的两倍

下面是HashMap的无参构造函数,只进行了一个赋值操作,并未创建table数组

 table数组是在 put 的时候创建的,put方法调用了putVal方法,在putVal方法中传入了hash(key),这是调用hash方法计算出key的哈希值,参数onlyIfAbsent 选择 false 表示当传入一个表中已存在的键(值可以不同)时,覆盖掉原来的键值对,选择true不覆盖

方法的返回值是V,发生覆盖的时候,put返回被覆盖的值,没有发生覆盖则返回null

putVal的源码如下, 我在源码中通过注释进行了一些说明

为什么在HashMap里面就有一个全局table,在这还要一个局部的tab呢?

        这是因为全局的table在对象中,需要跑到堆中访问,而方法中的局部变量存在于栈中,访问较块

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        
        //定义一个局部变量,用来记录哈希表中数组的地址值
        Node<K,V>[] tab; 

        //临时的第三方变量,用来记录键值对对象的地址值
        Node<K,V> p; 

        //表示当前数组长度
        int n;
        
        //表示索引
        int i;

        //这一步将哈希表中数组的地址值,赋值给局部变量tab
        if ((tab = table) == null || (n = tab.length) == 0)
            /**
                数组对象的创建就是在resize方法中完成
                resize方法主要完成了以下几个工作:
                1.如果当前是第一次添加数据,底层会创建一个长度为16,加载因子为0.75的数组
                2.如果不是第一次添加数据,会看数组中的元素是否到达了扩容的条件
                    如果没达到,不做任何操作,
                    如果达到,会新创建一个大小为原先两倍的新数组,并把数据全部转移到新数组中
            */
            n = (tab = resize()).length;

        /**
            i = (n - 1) & hash 将 数组的长度减一 和 键的哈希值 进行 与运算,
            得到当前键值对对象在数组中应存入的位置
            如果该位置是null,则通过newNode方法创建Node对象直接放入数组
            如果不是则走else
        */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;

            //判断要添加的键值对的键是否和数组中原本的键值对的键一样
            //如果相等则说明需要覆盖,用e记录下被覆盖的键值对
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            //判断数组中获取出来的键值对是不是红黑树中的节点
            //如果是,则调用方法putTreeVal,把当前的节点按红黑树的规则添加到树当中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            
            //如果不是红黑树,那就是链表,则按照下面的方法添加
            else {
                //这个for就是不断的往后查看节点
                for (int binCount = 0; ; ++binCount) {
                    //如果已经走到最后一个节点,则直接创建节点挂在后面
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        /**
                            判断当前链表长度是否超过8,如果超过8,就会调用方法treeifyBin
                            treeifyBin方法的底层会继续判断数组的长度是否大于等于64
                            如果同时满足这两个条件,就会把这个链表转成红黑树
                        */
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    //判断当前正在查看的键值对的键跟要插入的键值对的键是否一样
                    //如果一样,则表明需要进行覆盖,执行break,此时e记录要被覆盖的键值对
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            //如果e为null,表示当前不需要覆盖任何元素
            //如果e不为null,那么记录的就是要被覆盖的键值对
            //然后按照以下的方法进行覆盖,return 被替换的 值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //onlyIfAbsent 就是传入的第四个参数
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }

        //跟并发修改异常有关
        ++modCount;

        //threshold:记录的就是数组的长度 * 0.75 ,可以理解为哈希表的扩容时机
        if (++size > threshold)
            resize();

        //跟LinkHashMap有关
        afterNodeInsertion(evict);

        //表示当前没有覆盖任何元素,返回null
        return null;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值