HashMap 的 源码分析

简介: HashMap 的 源码分析

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

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

我们先来简单了解下哈希表的原理,下图中有一个数组,数组元素是键值对(为了对接HashMap,哈希表不一定存储键值对,其实可以简单的理解为:存储键值对的哈希表就是HashMap),

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

       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;
    }


目录
相关文章
|
6月前
|
Java
【JDK 源码分析】HashMap 操作方法
【1月更文挑战第27天】【JDK 源码分析】HashMap Put 元素 初始化
|
1月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
24 1
|
3月前
|
存储 缓存 安全
深度剖析Java HashMap:源码分析、线程安全与最佳实践
深度剖析Java HashMap:源码分析、线程安全与最佳实践
|
5月前
|
存储 Java 测试技术
滚雪球学Java(66):Java之HashMap详解:深入剖析其底层实现与源码分析
【6月更文挑战第20天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
45 3
滚雪球学Java(66):Java之HashMap详解:深入剖析其底层实现与源码分析
|
5月前
|
存储 算法 安全
JDK源码分析-HashMap
JDK源码分析-HashMap
|
6月前
|
存储 算法
HashMap源码分析
HashMap源码分析
|
6月前
|
安全 Java
【JDK 源码分析】HashMap 线程安全问题分析
【1月更文挑战第27天】【JDK 源码分析】HashMap 线程安全问题分析
|
6月前
|
存储 Java
【JDK 源码分析】HashMap 底层结构
【1月更文挑战第27天】【JDK 源码分析】HashMap 底层结构
|
6月前
|
存储 算法 Java
HashMap的源码分析(基于JDK1.8)
HashMap的源码分析(基于JDK1.8) Java中的HashMap是一种常用的数据结构,它是基于哈希表的数据结构,可以用来存储键值对。在HashMap中,每个键值对被称作一个Entry,每个Entry包含一个键和一个值。HashMap的实现基于数组和链表,数组用于存储Entry,链表用于解决哈希冲突。
|
存储 Java
[java进阶]——HashMap的底层实现原理和源码分析,另附几个高频面试题
[java进阶]——HashMap的底层实现原理和源码分析,另附几个高频面试题
166 0