精华内容
下载资源
问答
  • HashMap 底层

    2018-11-17 21:43:00
    HashMap 底层就是一个数组结构(Entry<K,V>[] table) 可以看出,Entry 就是数组中的元素,每个 Map.Entry 其实就是 一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了...

    HashMap 底层就是一个数组结构(Entry<K,V>[] table) 可以看出,Entry 就是数组中的元素,每个 Map.Entry 其实就是 一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。

    hashmap 的数据结构
    要知道 hashmap 是什么,首先要搞清楚它的数据结构,在 java 编程语言中,最基本的结构就是两种,一个是 数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap 也不例外。 Hashmap 实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“),请看下图(横排表 示数组,纵排表示数组元素【实际上是一个链表】)。
    在这里插入图片描述

    HashMap 的工作原理

    HashMap 基于 hashing 原理,我们通过 put()和 get()方法储存和获取对象。当我们将键值对传递给 put()方法 时,它调用键对象的 hashCode()方法来计算 hashcode,让后找到 bucket 位置来储存值对象。当获取对象时, 通过键对象的 equals()方法找到正确的键值对,然后返回值对象。HashMap 使用链表来解决碰撞问题,当发生 碰撞了,对象将会储存在链表的下一个节点中。 HashMap 在每个链表节点中储存键值对对象。

    当两个不同的键对象的 hashcode 相同时会发生什么?
    它们会储存在同一个 bucket 位置的链表中。键对象的 equals()方法用来找到键值对。

    因为 HashMap 的好处非常多,我曾经在电子商务的应用中使用 HashMap 作为缓存。因为金融领域非常多的运用 Java,也出于性能的考虑,我们会经常用到 HashMap 和 ConcurrentHashMap。

    展开全文
  • HashMap底层原理

    2019-09-28 11:52:32
    HashMap底层原理
  • HashMap底层实现原理详解

    万次阅读 多人点赞 2021-02-13 22:41:16
    文章目录前言一、快速入门二、使用步骤1.引入库2.读入数据总结学习内容:学习时间:学习产出:前言一、pandas是什么?二、使用步骤1.引入库2....随着JDK版本的跟新,JDK1.8对HashMap底层的实现进行


    一、快速入门

    示例:有一定基础的小伙伴们可以选择性的跳过该步骤

    HashMap是Java程序员使用频率最高的用于映射键值对(key和value)处理的数据类型。随着JDK版本的跟新,JDK1.8对HashMap底层的实现进行了优化,列入引入红黑树的数据结构和扩容的优化等。本文结合JDK1.7和JDK1.8的区别,深入探讨HashMap的数据结构实现和功能原理。
    Java为数据结构中的映射定义了一个接口java.uti.Map,此接口主要有四个常用的实现类,分别是HashMap,LinkedHashMap,Hashtable,TreeMap,IdentityHashMap。本篇文章主要讲解HashMap以及底层实现原理。

    1.HashMap的常用方法

    
    //        Hashmap存值:----------------------------------》 .put("key","value"); ----------》无返回值。
    //
    //        Hashmap取值:----------------------------------》 .get("key");-------------------》 返回Value的类型。
    //
    //        Hashmap判断map是否为空:-----------------------》 .isEmpty(); -------------------》返回boolean类型。
    //
    //        Hashmap判断map中是否存在这个key:--------------》.containsKey("key");------------》返回boolean类型。
    //
    //        Hashmap判断map中是否含有value:----------------》.containsValue("value");-------》返回boolean类型。
    //
    //        Hashmap删除这个key值下的value:----------------》.remove("key");-----------------》返回Value的类型。
    //
    //        Hashmap显示所有的value值:---------------------》.values(); --------------------》返回Value的类型。
    //
    //        Hashmap显示map里的值得数量:-------------------》.size(); ----------------------》返回int类型
    //
    //        HashMap显示当前已存的key:---------------------》 .keySet();-------------------》返回Key的类型数组。
    //
    //        Hashmap显示所有的key和value:-----------------》.entrySet());------------------》返回Key=Value类型数组。
    //
    //        Hashmap添加另一个同一类型的map:--------------》.putAll(map); -----------------》(参数为另一个同一类型的map)无返回值。
    //
    //        Hashmap删除这个key和value:------------------》.remove("key", "value");-------》(如果该key值下面对应的是该value值则删除)返回boolean类型。
    //
    //        Hashmap替换这个key对应的value值(JDK8新增):---》.replace("key","value");-------》返回被替换掉的Value值的类型。
    //
    //        克隆Hashmap:-------------------------------》.clone(); ---------------------》返回object类型。
    //
    //        清空Hashmap:-------------------------------》.clear(); ---------------------》无返回值。
    
    

    2.HashMap的几个重要知识点

    1. HashMap是无序且不安全的数据结构。

    2. HashMap 是以key–value对的形式存储的,key值是唯一的(可以为null),一个key只能对应着一个value,但是value是可以重复的。

    3. HashMap 如果再次添加相同的key值,它会覆盖key值所对应的内容,这也是与HashSet不同的一点,Set通过add添加相同的对象,不会再添加到Set中去。

    4. HashMap 提供了get方法,通过key值取对应的value值,但是HashSet只能通过迭代器Iterator来遍历数据,找对象。


    二、JDK7与JDK8的HashMap区别

    既然讲HashMap,那就不得不说一下JDK7与JDK8(及jdk8以后)的HashMap有什么区别:

    1. jdk8中添加了红黑树,当链表长度大于等于8的时候链表会变成红黑树
    2. 链表新节点插入链表的顺序不同(jdk7是插入头结点,jdk8因为要把链表变为红 黑树所以采用插入尾节点)
    3. hash算法简化 ( jdk8 )
    4. resize的逻辑修改(jdk7会出现死循环,jdk8不会)

    三、HashMap的容量与扩容机制

    1.HashMap的默认负载因子

        /**
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        /**
         *默认的负载因子是0.75f,也就是75% 负载因子的作用就是计算扩容阈值用,比如说使用
         *无参构造方法创建的HashMap 对象,他初始长度默认是16  阈值 = 当前长度 * 0.75  就
         *能算出阈值,当当前长度大于等于阈值的时候HashMap就会进行自动扩容
         */
    
    

    面试的时候,面试官经常会问道一个问题:为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?
    答案有两种:

    1. 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。

    2. 理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的

    2.HashMap的扩容机制

    写数据之后会可能触发扩容,HashMap结构内,我记得有一个记录当前数据量的字段,这个数据量字段到达扩容阈值的话,它就会触发扩容的操作

    阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 
    当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。
    
    扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
    假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前
    容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2
    

    问题又来了,为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?
    这个问题就比较简单了,因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。

    3.HashMap中散列表数组初始长度

        /**
         * The default initial capacity - MUST be a power of two.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        /**
         * HashMap中散列表数组初始长度为 16 (1 << 4)
         * 创建HashMap的时候可以设置初始化容量和设置负载因子,
         * 但HashMap会自动优化设置的初始化容量参数,确保初始化
         * 容量始终为2的幂
         */
    
    

    老问题又来了,为啥HashMap中初始化大小为什么是16呢?

    首先我们看hashMap的源码可知当新put一个数据时会进行计算位于table数组(也称为桶)中的下标:

    int index =key.hashCode()&(length-1);

    hahmap每次扩容都是以 2的整数次幂进行扩容

    因为是将二进制进行按位于,(16-1) 是 1111,末位是1,这样也能保证计算后的index既可以是奇数也可以是偶数,并且只要传进来的key足够分散,均匀那么按位于的时候获得的index就会减少重复,这样也就减少了hash的碰撞以及hashMap的查询效率。

    那么到了这里你也许会问? 那么就然16可以,是不是只要是2的整数次幂就可以呢?

    答案是肯定的。那为什么不是8,4呢? 因为是8或者4的话很容易导致map扩容影响性能,如果分配的太大的话又会浪费资源,所以就使用16作为初始大小。


    四、HashMap的结构

    JDK7与JDK8及以后的HashMap结构与存储原理有所不同:
    Jdk1.7:数组 + 链表 ( 当数组下标相同,则会在该下标下使用链表)
    Jdk1.8:数组 + 链表 + 红黑树 (预值为8 如果链表长度 >=8则会把链表变成红黑树 )
    Jdk1.7中链表新元素添加到链表的头结点,先加到链表的头节点,再移到数组下标位置
    Jdk1.8中链表新元素添加到链表的尾结点
    (数组通过下标索引查询,所以查询效率非常高,链表只能挨个遍历,效率非常低。jdk1.8及以
    上版本引入了红黑树,当链表的长度大于或等于8的时候则会把链表变成红黑树,以提高查询效率)


    五、HashMap存储原理与存储流程

    1.HashMap存储原理

    1. 获取到传过来的key,调用hash算法获取到hash值

    2. 获取到hash值之后调用indexFor方法,通过获取到的hash值以及数组的长度算
      出数组的下标 (把哈希值和数组容量转换为二进,再在数组容量范围内与哈希值
      进行一次与运算,同为1则1,不然则为0,得出数组的下标值,这样可以保证计算出的数组下标不会大于当前数组容量)

    3. 把传过来的key和value存到该数组下标当中。

    4. 如该数组下标下以及有值了,则使用链表,jdk7是把新增元素添加到头部节点 jdk8则添加到尾部节点。

    2.HashMap存储流程

    前面寻址算法都是一样的,根据key的hashcode经过高低位异或之后的值,再按位与 &(table.lingth - 1),得到一个数组下标,然后根据这个数组下标内的状况,状况不同,然后情况也不同,大概分为了4种状态:

    ( 1.)第一种就是数组下标下内容为空:
    这种情况没什么好说的,为空据直接占有这个slot槽位就好了,然后把当前.put方法传进来的key和value包装成一个node对象,放到这个slot中就好了。

    ( 2.)第二种情况就是数组下标下内容不为空,但它引用的node还没有链化:
    这种情况下先要对比一下这个node对象的key与当前put对象的key是否完全.相等,如果完全相等的情况下,就行进行replace操作,把之前的槽位中node.下的value替换成新的value就可以了,否则的话这个put操作就是一个正儿.八经的hash冲突,这种情况在slot槽位后面追加一个node就可以了,用尾插法 ( 前面讲过,jdk7是把新增元素添加到头部节点,而jdk8则添加到尾部节点)。

    ( 3.)第三种就是该数组下标下内容已经被链化了:
    这种情况和第二种情况处理很相似,首先也是迭代查找node,看看链表上中元素的key,与当前传过来的key是否完全一致,如果完全一致的话还是repleace操作,用put过来的新value替换掉之前node中的value,否则的话就是一致迭代到链表尾节点也没有匹配到完全一致的node,就和之前的一样,把put进来数据包装成node追加到链表的尾部,再检查一下当前链表的长度,有没有达到树化阈值,如果达到了阈值就调用一个树化方法,树化操作都是在这个方法里完成的。

    ( 4.)第四种情况就是冲突很严重的情况下,这个链表已经转化成红黑树了:
    红黑树就比较复杂 要将清楚这个红黑树还得从TreeNode说起 TreeNode继承了Node结构,在Node基础上加了几个字段,分别是指向父节点parent字段,指向左子节点left字段,指向右子节点right字段,还有一个表示颜色的red字段,这就是TreeNode的基本结构,然后红黑树的插入操作,首先找到一个合适的插入点,就是找到插入节点的父节点,然后红黑树它又满足二叉树的所有特性,所以找这个父节点的操作和二叉树排序是完全一致的,然后说一下这个二叉树排序,其实就是二分查找算法映射出来的结构,就是一个倒立的二叉树,然后每个节点都可以有自己的子节点,本且左节点小于但前节点,右节点大于当前节点,然后每次向下查找一层就能那个排除掉一半的数据,查找效率非常的高效,当查找的过程中也是分情况的。

    1. 首先第一种情况就是一直向下探测,直到查询到左子树或者右子树位null,说明整个树中,并没有发现node链表中的key与当前put key一致的TreeNode,那此时探测节点就是插入父节点的所在了,然后就是判断插入节点的hash值和父节点的hash值大小决定插入到父节点的左子树还是右子树。当然插入会打破平衡,还需要一个红黑树的平衡算法保持平衡。

    2. 其次第二种情况就是根节点在向下探测过程中发现TreeNode中key与当前put的key完全一致,然后就也是一次repleace操作,替换value。


    六、jdk8中HashMap为什么要引入红黑树?

    其实主要就是为了解决jdk1.8以前hash冲突所导致的链化严重的问题,因为链表结构的查询效率是非常低的,他不像数组,能通过索引快速找到想要的值,链表只能挨个遍历,当hash冲突非常严重的时候,链表过长的情况下,就会严重影响查询性能,本身散列列表最理想的查询效率为O(1),当时链化后链化特别严重,他就会导致查询退化为O(n)为了解决这个问题所以jdk8中的HashMap添加了红黑树来解决这个问题,当链表长度>=8的时候链表就会变成红黑树,红黑树其实就是一颗特殊的二叉排序树嘛,这个时间复杂…反正就是要比列表强很多


    七、扩容后的新table数组,那老数组中的这个数据怎么迁移呢

    迁移其实就是挨个桶位推进迁移,就是一个桶位一个桶位的处理,主要还是看当前处理桶位的数据状态把,这里也是分了大概四种状态:
    这四种的迁移规则都不太一样

    (1.)第一种就是数组下标下内容为空:
    这种情况下就没什么可说的,不用做什么处理。

    ( 2.)第二种情况就是数组下标下内容不为空,但它引用的node还没有链化:
    当slot它不为空,但它引用的node还没有链化的时候,说明这个槽位它没有发生过hash冲突,直接迁移就好了,根据新表的tableSize计算出他在新表的位置,然后存放进去就好了。

    ( 3.)第三种就是slot内储存了一个链化的node:
    当node中next字段它不为空,说明槽位发生过hash冲突,这个时候需要把当前槽位中保存的这个链表拆分成两个链表,分别是高位链和低位链

    (4.)第四种就是该槽位储存了一个红黑树的根节点TreeNode对象:
    这个就很复杂了,本文章暂时不做过多的介绍(博主还没整明白 =_=! )




    展开全文
  • HashMap底层实现原理解析

    万次阅读 多人点赞 2020-12-18 10:37:27
    一:HashMap底层实现原理解析 我们常见的有数据结构有三种结构:1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各自的数据结构的特点: 1、数组结构: 存储区间连续、内存占用严重、空间复杂度大 优点:...

    一:HashMap底层实现原理解析

    我们常见的有数据结构有三种结构:1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各自的数据结构的特点:
    1、数组结构: 存储区间连续、内存占用严重、空间复杂度大

    • 优点:随机读取和修改效率高,原因是数组是连续的(随机访问性强,查找速度快)
    • 缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中都要往后移动,且大小固定不易动态扩展。

    2、链表结构:存储区间离散、占用内存宽松、空间复杂度小

    • 优点:插入删除速度快,内存利用率高,没有固定大小,扩展灵活
    • 缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)

    3、哈希表结构:结合数组结构和链表结构的优点,从而实现了查询和修改效率高,插入和删除效率也高的一种数据结构
    常见的HashMap就是这样的一种数据结构
    在这里插入图片描述

    HashMap中的put()和get()的实现原理

    • 1、map.put(k,v)实现原理
      (1)首先将k,v封装到Node对象当中(节点)。
      (2)然后它的底层会调用K的hashCode()方法得出hash值。
      (3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
    • 2、map.get(k)实现原理
      (1)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
      (2)通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

    为何随机增删、查询效率都很高的原因是?
    原因: 增删是在链表上完成的,而查询只需扫描部分,则效率高。

    HashMap集合的key,会先后调用两个方法,hashCode and equals方法,这这两个方法都需要重写。

    为什么放在hashMap集合key部分的元素需要重写equals方法?
    因为equals方法默认比较的是两个对象的内存地址

    二:HashMap红黑树原理分析

    相比 jdk1.7 的 HashMap 而言,jdk1.8最重要的就是引入了红黑树的设计,当hash表的单一链表长度超过 8 个的时候,链表结构就会转为红黑树结构。
    为什么要这样设计呢?好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢。
    在这里插入图片描述

    • 红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn);
    • 链表查询:这种情况下,需要遍历全部元素才行,时间复杂度 O(n);

    简单的说,红黑树是一种近似平衡的二叉查找树,其主要的优点就是“平衡“,即左右子树高度几乎一致,以此来防止树退化为链表,通过这种方式来保障查找的时间复杂度为 log(n)。
    在这里插入图片描述
    关于红黑树的内容,网上给出的内容非常多,主要有以下几个特性:

    • 1、每个节点要么是红色,要么是黑色,但根节点永远是黑色的;

    • 2、每个红色节点的两个子节点一定都是黑色;

    • 3、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);

    • 4、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

    • 5、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);

    在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件 3 或条件 4,需要通过调整使得查找树重新满足红黑树的条件。

    展开全文
  • HashMap底层实现和原理(源码解析)

    万次阅读 多人点赞 2019-06-15 10:14:34
    Note:文章的内容基于JDK1.7进行分析,1.8做的改动文章末尾进行...HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不...

    Note:文章的内容基于JDK1.7进行分析,1.8做的改动文章末尾进行讲解。

    大家可以看一下:https://www.imooc.com/article/267756

    一、先来熟悉一下我们常用的HashMap

    1、概述

    HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。

    2、继承关系

    public class HashMap<K,V>extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable
     

    3、基本属性

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化大小 16 
    static final float DEFAULT_LOAD_FACTOR = 0.75f;     //负载因子0.75
    static final Entry<?,?>[] EMPTY_TABLE = {};         //初始化的默认数组
    transient int size;     //HashMap中元素的数量
    int threshold;          //判断是否需要调整HashMap的容量  

    Note:HashMap的扩容操作是一项很耗时的任务,所以如果能估算Map的容量,最好给它一个默认初始值,避免进行多次扩容。HashMap的线程是不安全的,多线程环境中推荐是ConcurrentHashMap。

    二、常被问到的HashMap和Hashtable的区别

    1、线程安全

    两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。

    Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

    Note:

    Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理。

    2、针对null的不同

    HashMap可以使用null作为key,而Hashtable则不允许null作为key
    虽说HashMap支持null值作为key,不过建议还是尽量避免这样使用,因为一旦不小心使用了,若因此引发一些问题,排查起来很是费事。
    Note:HashMap以null作为key时,总是存储在table数组的第一个节点上。

    3、继承结构

    HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类。

    4、初始容量与扩容

    HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。

    HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1。

    5、两者计算hash的方法不同

    Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模

    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;

    HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。

    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    
    static int hash(int h) {
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
     
     static int indexFor(int h, int length) {
            return h & (length-1);

    三、HashMap的数据存储结构

    1、HashMap由数组和链表来实现对数据的存储

    HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。

    数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

    链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

     

     

     

    从上图我们可以发现数据结构由数组+链表组成,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key.hashCode())%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。

    HashMap里面实现一个静态内部类Entry,其重要的属性有 hash,key,value,next。

    HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。

     public V put(K key, V value) {
            if (key == null)
                return putForNullKey(value); //null总是放在数组的第一个链表中
            int hash = hash(key.hashCode());
            int i = indexFor(hash, table.length);
            //遍历链表
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                //如果key在链表中已存在,则替换为新value
                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;
        }
    
     
    
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next
        //如果size超过threshold,则扩充table大小。再散列
        if (size++ >= threshold)
                resize(2 * table.length);
    }

    四、重要方法深度解析

    1、构造方法

    HashMap()    //无参构造方法
    HashMap(int initialCapacity)  //指定初始容量的构造方法 
    HashMap(int initialCapacity, float loadFactor) //指定初始容量和负载因子
    HashMap(Map<? extends K,? extends V> m)  //指定集合,转化为HashMap

    HashMap提供了四个构造方法,构造方法中 ,依靠第三个方法来执行的,但是前三个方法都没有进行数组的初始化操作,即使调用了构造方法此时存放HaspMap中数组元素的table表长度依旧为0 。在第四个构造方法中调用了inflateTable()方法完成了table的初始化操作,并将m中的元素添加到HashMap中。

    2、添加方法

    public V put(K key, V value) {
            if (table == EMPTY_TABLE) { //是否初始化
                inflateTable(threshold);
            }
            if (key == null) //放置在0号位置
                return putForNullKey(value);
            int hash = hash(key); //计算hash值
            int i = indexFor(hash, table.length);  //计算在Entry[]中的存储位置
            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); //添加到Map中
            return null;
    }

    在该方法中,添加键值对时,首先进行table是否初始化的判断,如果没有进行初始化(分配空间,Entry[]数组的长度)。然后进行key是否为null的判断,如果key==null ,放置在Entry[]的0号位置。计算在Entry[]数组的存储位置,判断该位置上是否已有元素,如果已经有元素存在,则遍历该Entry[]数组位置上的单链表。判断key是否存在,如果key已经存在,则用新的value值,替换点旧的value值,并将旧的value值返回。如果key不存在于HashMap中,程序继续向下执行。将key-vlaue, 生成Entry实体,添加到HashMap中的Entry[]数组中。

    3、addEntry()

    /*
     * hash hash值
     * key 键值
     * value value值
     * bucketIndex Entry[]数组中的存储索引
     * / 
    void addEntry(int hash, K key, V value, int bucketIndex) {
         if ((size >= threshold) && (null != table[bucketIndex])) {
             resize(2 * table.length); //扩容操作,将数据元素重新计算位置后放入newTable中,链表的顺序与之前的顺序相反
             hash = (null != key) ? hash(key) : 0;
             bucketIndex = indexFor(hash, table.length);
         }
    
        createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

    添加到方法的具体操作,在添加之前先进行容量的判断,如果当前容量达到了阈值,并且需要存储到Entry[]数组中,先进性扩容操作,空充的容量为table长度的2倍。重新计算hash值,和数组存储的位置,扩容后的链表顺序与扩容前的链表顺序相反。然后将新添加的Entry实体存放到当前Entry[]位置链表的头部。在1.8之前,新插入的元素都是放在了链表的头部位置,但是这种操作在高并发的环境下容易导致死锁,所以1.8之后,新插入的元素都放在了链表的尾部。

    4、获取方法:get

    public V get(Object key) {
         if (key == null)
             //返回table[0] 的value值
             return getForNullKey();
         Entry<K,V> entry = getEntry(key);
    
         return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
         if (size == 0) {
             return null;
         }
    
         int hash = (key == null) ? 0 : hash(key);
         for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
             Object k;
             if (e.hash == hash &&
                 ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
          }
         return null;
    }

    在get方法中,首先计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值。

    5、删除方法

    public V remove(Object key) {
         Entry<K,V> e = removeEntryForKey(key);
         return (e == null ? null : e.value);
    }
    final Entry<K,V> removeEntryForKey(Object key) {
         if (size == 0) {
             return null;
         }
         int hash = (key == null) ? 0 : hash(key);
         int i = indexFor(hash, table.length);
         Entry<K,V> prev = table[i];
         Entry<K,V> e = prev;
    
         while (e != null) {
             Entry<K,V> next = e.next;
             Object k;
             if (e.hash == hash &&
                 ((k = e.key) == key || (key != null && key.equals(k)))) {
                 modCount++;
                 size--;
                 if (prev == e)
                     table[i] = next;
                 else
                     prev.next = next;
                 e.recordRemoval(this);
                 return e;
             }
             prev = e;
             e = next;
        }
    
        return e;
    }

    删除操作,先计算指定key的hash值,然后计算出table中的存储位置,判断当前位置是否Entry实体存在,如果没有直接返回,若当前位置有Entry实体存在,则开始遍历列表。定义了三个Entry引用,分别为pre, e ,next。 在循环遍历的过程中,首先判断pre 和 e 是否相等,若相等表明,table的当前位置只有一个元素,直接将table[i] = next = null 。若形成了pre -> e -> next 的连接关系,判断e的key是否和指定的key 相等,若相等则让pre -> next ,e 失去引用。

    6、containsKey

    public boolean containsKey(Object key) {
            return getEntry(key) != null;
        }
    final Entry<K,V> getEntry(Object key) {
            int hash = (key == null) ? 0 : hash(key.hashCode());
            for (Entry<K,V> e = table[indexFor(hash, table.length)];
                 e != null;
                 e = e.next) {
                Object k;
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            return null;
        }

    containsKey方法是先计算hash然后使用hash和table.length取摸得到index值,遍历table[index]元素查找是否包含key相同的值。

    7、containsValue

    public boolean containsValue(Object value) {
        if (value == null)
                return containsNullValue();
    
        Entry[] tab = table;
            for (int i = 0; i < tab.length ; i++)
                for (Entry e = tab[i] ; e != null ; e = e.next)
                    if (value.equals(e.value))
                        return true;
        return false;
        }

    containsValue方法就比较粗暴了,就是直接遍历所有元素直到找到value,由此可见HashMap的containsValue方法本质上和普通数组和list的contains方法没什么区别,你别指望它会像containsKey那么高效。

    五、JDK 1.8的 改变

    1、HashMap采用数组+链表+红黑树实现。

    在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变得,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。

    2、put方法简单解析:

    public V put(K key, V value) {
        //调用putVal()方法完成
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断table是否初始化,否则初始化操作
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //计算存储的索引位置,如果没有元素,直接赋值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            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);
                        //链表长度8,将链表转化为红黑树存储
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //key存在,直接覆盖
                    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;
            }
        }
        //记录修改次数
        ++modCount;
        //判断是否需要扩容
        if (++size > threshold)
            resize();
        //空操作
        afterNodeInsertion(evict);
        return null;
    }

    如果存在key节点,返回旧值,如果不存在则返回Null。

    展开全文
  • HashMap底层实现原理及面试问题

    万次阅读 多人点赞 2018-08-29 11:00:56
    HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取...
  • HashMap底层原理.md

    2021-06-16 10:57:05
    HashMap底层原理.md
  • HashMap 底层原理

    2020-05-23 14:53:40
    HashMap 底层原理
  • HashMap底层实现原理

    万次阅读 多人点赞 2019-04-24 13:57:56
    HashMap底层实现原理 HashMap是Java语言中用的最频繁的一种数据结构。 1.hashmap的数据结构 要了解hashmap首先要弄清楚他的结构。在java编程语言中最基本的数据结构有两种,数组和链表。 数组:查询速度快,可以根据...
  • HashMap 底层分析

    2019-03-14 08:29:00
    HashMap 底层分析 以下基于 JDK1.7 分析。 image 如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数: 容量 负载因子 容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size >...
  • HashMap底层总结

    2020-08-20 15:53:25
    HashMap底层数据结构 HashMap底层使用数组、链表、红黑树(jdk1.8),可以实现快速存储、快速查找、可伸缩三大特点。 三大特点解析 快速存储:HashMap使用位运算,hash & 数组长度-1的方法快速定位到数据的存储...
  • hashmap底层原理

    2020-08-06 23:10:46
    hashmap底层原理 hashmap在Java中被频繁使用到 ,了解hashmap底层原理有利于我们理解它的存储过程及快速使用。 1.哈希表 首先我们来认识一下哈希表,哈希表:是根据关键码值(Key value)而直接进行访问的数据结构。也...
  • HashMap底层分析

    2019-03-24 19:53:34
    HashMap 底层分析 以下基于 JDK1.7 分析。 如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数: 容量 负载因子 容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size > 16*0.75 时...
  • HashMap底层实现

    2020-09-02 16:20:26
    HashMap底层实现 在JDK1.8之前,HashMap的底层是数组加链表组成,也就是散列链表。HashMap通过key的HashCode经过扰动函数处理后得到hash值,这个扰动函数就是HashMap中的hash()函数,然后通过(n-1)&hash判断当前...
  • hashmap 底层实现原理

    2021-03-04 11:06:45
    Java HashMap底层实现原理 HashMap底层是哈希表(散列表),哈希就是一个数组,数组的每个元素是一个单向链表。 ● 在第一次执行put方法时,给哈希表的数组(哈希桶)默认初始化,容量: 16 ● hashMap加载因子是0.75 ● ...
  • HashMap底层实现详解

    2019-08-20 09:14:34
    HashMap底层实现详解前言Java中的hashCode和equals关于hashCode关于equalsHashMap的实现原理Hashmap基本结构讲解存储数据过程put(key,value)HashMap的resize取数据过程get(key)扩容问题 前言 HashMap底层实现采用了...
  • HashMap底层原理详解 本文旨在系统学习常用容器 HashMap 的使用及底层实现原理,欢迎指正。 文章目录HashMap底层原理详解前言一、HashMap 底层数据结构二、HashMap 常见操作三、HashMap 重要知识点 前言 HashMap ...
  • HashMap底层详解

    千次阅读 2016-06-12 17:35:11
    HashMap底层详解,看完就明白了
  • hashmap底层数据结构

    2020-08-10 10:49:44
    HashMap底层数据结构 JDK1.7及之前:数组+链表 JDK1.8:数组+链表+红黑树

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,205
精华内容 5,282
关键字:

hashmap底层