HashMap原理及put方法与get方法的调用过程

HashMap的原理

HashMap的数据结构为数组+链表,以key,value的形式存值,通过调用put与get方法来存值与取值。

它内部维护了一个Entry数组,得到key的hashCode值将其移位按位与运算,然后再通过跟数组的长度-1作逻辑与运算得到一个index值来确定数据存储在Entry数组当中的位置,通过链表来解决hash冲突问题。

当发生碰撞了,对象将会储存在链表的下一个节点中。

HashMap底层原理(当你put,get时内部会发生什么呢?)

接触过HashMap的小伙伴都会经常使用put和get这些方法,那接下来就对HashMap的内部存储进行详解.(以初学者的角度进行分析)-(小白篇)

当程序试图将多个 key-value 放入 HashMap 中时,以如下代 码片段为例:

上面代码,创建了一个HashMap对象,并且指定了容量(capacity)和负载因子(loadFactor),然后put,以键值对的方式储存值. 容量咱们很容易理解(默认16容量),也就是给它一个初始化的长度,那么负载因子又是个啥?

负载因子 : 表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容.(这里我把负载因子设置到0.9f,这么做的原因是想让"效果"更明显,啥效果,后面讲解.) 具体扩容多少,源码有这样一段代码如下:

我们从这里可以知道阈(yu)值的计算公式:

阈值(threshold) = 负载因子(loadFactor) * 容量(capacity)

来,上源码 如下:

这是源码的构造函数,来看看最后一行代码用 tableSizeFor(initialCapacity) 方法来计算出阈值,

查看此方法源码 如下:

cap

参数也就是给的初始容量,这段算法会给出一个 距离参数cap 最近的并且没有变小的 2 的幂次方数,比如传入10 返回 16,就是这么神奇!

以上我们了解了HashMap的扩容机制,也知道了创建一个HashMap对象的内部活动. 下面我们对put添加一个键值对的方法进行解析.

我们知道HashMap是以key-value的形式保存的,取用get()方法查找key来获取相对应的value. 我们可以调试put值时看出HashMap底层是用数组构成的,并且存放的位置是散列无序的,这点不像数组按存放的先后顺序来排列.如下图:

当put完第4个值时发现只显示了3个元素,之后一个个点开元素后发现,第4个元素出现在next这个属性中. 如下图:

然后继续put完全部值,在看,一共存放了12个值,但是table中只有9个元素,还发现阈(yu)[ threshold ]值从最初的7增加到了15,容量(capacity)也从原来给的8变成了16,说明触发了扩容机制(从源代码可看到容量扩充至原来的二倍),在一个我们刚刚发现了有些值跑到了另一些值的next属性里去了.我们点开元素的next属性看看,是不是跑到这里头了.如下图:

果然,这三俩跑人家的底盘来了.在下标 7,13,14中的Next的属性中找到了"遗失"的三个元素.

在看如下图:

仔细瞅瞅,发现每个元素都有这么一个next的属性,有些为空,有些不为空,不为空的则是元素存放在此next中,有没有感觉元素被next属性组成了一条链子.来上图(形象又生动):

此图模拟了内部的结构方式,在同一下标中同时存在多个元素,产生了链表结构图中的箭头也就表示着每个元素中的next属性,看到这会发现许多诡异所思的问题, 为啥它存储是无序的呢? , 为啥存着存着都跑到一块去了,成了链表结构呢?,等一些问题.咱们下面通过源码来看看.(源码如下):

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

既然叫HashMap,当然得和Hash扯点关系啦.

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。小伙伴可以试试调用hashCode()方法看看经过此算法会得出怎样的结果.

咱们现在知道为啥是无序存放的了,key通过哈希算法的值来决定它存储的位置,那出现的重叠现象表明,不同的key经过哈希算法得出的值会出现相等的可能(这样的现状称为碰撞/冲突),所以一个下标会出现多个元素,形成链表结构.至于为什么采用链表,是为了节省空间,链表在内存中并不是连续存储,所以我们可以更充分地使用内存。

(下面我们将每个下标统称为Entry(桶),也就是一个 key-value 对)

有没有觉得这样会降低查询的效率(链表),进行查询时,先查找到Entry,在通过链的遍历.想着都觉得麻烦,虽然这样解决了碰撞这样的冲突,但是引来了一个大毛病(查找效率降低),这得不行啊,人家HashMap同志就是以快出名啊,所以在jdk8中进行了优化, 引入了树结构,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度,,来优化 链 过长所带来的性能低化的问题.

来上码,继续查看putVal(hash(key), key, value, false, true); 的源码:

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        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);
                        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;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

咱们看完注释应该了解它的大概了,继续查看treeifyBin()将链表改为红黑树 (jdk8新特性)方法码:

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 如果数组等于null 或 数组长度小于 64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 重新散列,使得链表变短
            resize();
        // 如果hash冲突,且数组长度大于 64,则只能使用红黑树结构
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
             // 返回新的红黑树
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

以上介绍了MashMap对存储数据的机制进行简短的介绍.我们已经知道产生碰撞会导致查询效率打折扣,那么如何能有效的避免哈希碰撞呢?

咱们先反向思维一下,你认为什么情况会导致HashMap的哈希碰撞比较多?

无外乎两种情况:

1、容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争强。

2、hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争抢。

所以,解决HashMap中的哈希碰撞也是从这两方面入手。

这两点在HashMap中都有很好的提现。两种方法相结合,在合适的时候扩大数组容量,再通过一个合适的hash算法计算元素分配到哪个数组中,就可以大大的减少冲突的概率。但数据量大时,碰撞也会成正比的增长,所以引入红黑树的结构,就能避免查询效率低下的问题。

咱们再来看看负载因子这个影响性能的平衡点有啥规律.上文已经对啥是负载因子进行了解释.

它Hsah表中元素的填满的程度.

若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。

反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

冲突的机会越大,则查找的成本越高.

因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.

这里写了段测试代码 如下:

public class HashTest {
 public static void main(String[] args) {
  // 对"负载因子的大小对程序的影响规律"进行测试
  // threshold=capacity * loadFactor ---- 阈值 = 容量 x 负载因子
  // 源代码扩容后容量是扩容前的二倍
  int n1 = 10;   // 对照组
  int n2 = 1000000; // put/get多少组
  long t0 = 0;      //总耗时
  float lf = 0.9f;  //负载因子
  int capacity = 100; //初始容量
  HashMap map = null;

  //对照组循环
  for (int j = 1; j <= n1; j++) {
   map = new HashMap(capacity, lf);
   List<String> list = new ArrayList<String>();
   // 利用循环进行put
   for (int i = 0; i < n2; i++) {
    String temp = HashTest.randomString();
    map.put(temp, i);
    list.add(temp);
   }
   long time = 0; // 总耗费时间
   // 利用循环get
   for (int i = 0; i < n2; i++) {
    String temp = list.get(i);
    long t1 = System.currentTimeMillis();
    map.get(temp);
    long t2 = System.currentTimeMillis();
    long t3 = t2 - t1;// 花费时间
    time += t3;
   }
   System.out.println("组"+j+"花费时间(ms)=" + time);
   t0 += time;
   map = null;
  }
  System.out.println("get出 "+n2+" 对键值对中,"+n1+"组数据得出:");
  System.out.println("---------------------------------");
  System.out.println("每get"+n2+"对键值对 平均花费时间(毫秒):"+(t0/n1));
 }
 /**
  * 产生随机字符串方法
  * @return
  */
 public static String randomString() {
  // 最终产生的字符串
  StringBuffer sb = new StringBuffer();
  // 字符串样本
  String str = "回到家卡萨恒大帝景阿萨德节快乐就看见了困窘企业无辜的鄙视你别这么想按一个预告的哈上东国际按时大大伽伽汇顶科技啊啥看的撒打算大的欧亚报出去qwertyuiopasdfghjklzxvcbnm,.;p[']/\1234567890zxcvbnmaksjhfgdlpoiuytrewq阿斯加德克拉斯近段时间的书上方法更符合辅导费的冠福股份极乐空间流口水";
  // System.out.println("样本字符串长度:"+str.length());
  // 产生一个1到30的数字
  int num = (int) (Math.random() * 30 + 1);
  // System.out.println("num="+num);
  // 用for循环从样本字符串中提取出字符进行组合
  for (int i = 0; i < num; i++) {
   int num1 = (int) (Math.random() * str.length()); // 产生一个0到字符串样本的数字
   // 根据索引值获取对应的字符
   char charAt = str.charAt(num1);
   sb.append(charAt);
  }
  // System.out.println("产生一个长度为"+num+"的字符串");
  return sb.toString();
 }

小伙伴可以调节负载因子的大小来测试,时间上的差异.

我们可以发现,为了保证哈希的结果可以分散、为了提高哈希的效率,JDK在一个小小的hash方法上就有很多考虑,做了很多事情。当然,我希望我们不仅可以深入了解背后的原理,还要学会这种对代码精益求精的态度。

Jdk的源代码,每一行都很有意思,都值得花时间去钻研、推敲。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • HashMap底层实现原理详解

    一.快速入门 示例:有一定基础的小伙伴们可以选择性的跳过该步骤 HashMap是Java程序员使用频率最高的用于映射键值对(key和value)处理的数据类型.随着JDK版本的跟新,JDK1.8对HashMap底层的实现进行了优化,列入引入红黑树的数据结构和扩容的优化等.本文结合JDK1.7和JDK1.8的区别,深入探讨HashMap的数据结构实现和功能原理. Java为数据结构中的映射定义了一个接口java.uti.Map,此接口主要有四个常用的实现类,分别是HashMap,LinkedHas

  • java HashMap 的工作原理详解

    HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如此特殊呢?是因为这道题考察的深度很深.这题经常出现在高级或中高级面试中.投资银行更喜欢问这个问题,甚至会要求你实现HashMap来考察你的编程能力.ConcurrentHashMap和其它同步集合的引入让这道题变得更加复杂.让我们开始探索的旅程吧! 先来些简单的问题 "你用过HashMap吗?&quo

  • HashMap原理的深入理解

    hashing(散列法或哈希法)的概念 散列法(Hashing)是一种将字符组成的字符串转换为固定长度(一般是更短长度)的数值或索引值的方法,称为散列法,也叫哈希法.由于通过更短的哈希值比用原始值进行数据库搜索更快,这种方法一般用来在数据库中建立索引并进行搜索,同时还用在各种解密算法中. HashMap概念和底层结构 HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.HashMap储存的是键值对,HashMap很快.此类不保证映射

  • Java源码解析之HashMap的put、resize方法详解

    一.HashMap 简介 HashMap 底层采用哈希表结构 数组加链表加红黑树实现,允许储存null键和null值 数组优点:通过数组下标可以快速实现对数组元素的访问,效率高 链表优点:插入或删除数据不需要移动元素,只需要修改节点引用效率高 二.源码分析 2.1 继承和实现 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

  • HashMap原理及put方法与get方法的调用过程

    HashMap的原理 HashMap的数据结构为数组+链表,以key,value的形式存值,通过调用put与get方法来存值与取值. 它内部维护了一个Entry数组,得到key的hashCode值将其移位按位与运算,然后再通过跟数组的长度-1作逻辑与运算得到一个index值来确定数据存储在Entry数组当中的位置,通过链表来解决hash冲突问题. 当发生碰撞了,对象将会储存在链表的下一个节点中. HashMap底层原理(当你put,get时内部会发生什么呢?) 接触过HashMap的小伙伴都会经

  • Referer原理与图片防盗链实现方法详解

    本文实例讲述了Referer原理与图片防盗链实现方法.分享给大家供大家参考,具体如下: 1.图片防盗链 在一些大型网站中,比如百度贴吧,该站点的图片采用了防盗链的规则,以至于使用下面代码会发生错误. 简单代码: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="

  • 朴素贝叶斯分类算法原理与Python实现与使用方法案例

    本文实例讲述了朴素贝叶斯分类算法原理与Python实现与使用方法.分享给大家供大家参考,具体如下: 朴素贝叶斯分类算法 1.朴素贝叶斯分类算法原理 1.1.概述 贝叶斯分类算法是一大类分类算法的总称 贝叶斯分类算法以样本可能属于某类的概率来作为分类依据 朴素贝叶斯分类算法是贝叶斯分类算法中最简单的一种 注:朴素的意思是条件概率独立性 P(A|x1x2x3x4)=p(A|x1)*p(A|x2)p(A|x3)p(A|x4)则为条件概率独立 P(xy|z)=p(xyz)/p(z)=p(xz)/p(z)

  • java中hashCode方法与equals方法的用法总结

    首先,想要明白hashCode的作用,必须要先知道Java中的集合. 总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set. 前者集合内的元素是有序的,元素可以重复:后者元素无序,但元素不可重复. 那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢? 这就是Object.equals方法了.但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了. 也就是说,如果集合中现在已经有

  • 通过HashMap原理详解entrySet中的疑问

    目录 HashMap底层变量 put()方法: 2. get(Object key)方法: 3. remove(Object key)方法: 4.entrySet()方法: EntrySet类代码 HashMap底层变量 HashMap的底层的一些变量: transient Node<K,V>[] table; //存储数据的Node数组 transient Set<java.util.Map.Entry<K,V>> entrySet; transient int si

  • Javascript 面向对象(一)(共有方法,私有方法,特权方法)

    私有方法:私有方法本身是可以访问类内部的所有属性的,即私有属性和公有属性.但是私有方法是不可以在类的外部被调用. 私有方法写法: 复制代码 代码如下: function myClass () { var private_attribute = initial_value; function private_method () { } var private_method2 = function () { } } 实例showpet()就是一个私有方法 复制代码 代码如下: var pet=fun

  • php生成静态html页面的方法(2种方法)

    因为每次用户点击动态链接的时候都会对服务器发送数据查询的要求,对于一个访问量可能达百万千万级别的网站来说 这无疑是服务器一个大大的负担,所以把动态数据转换成静态html页面就成了节省人力物力的首选. 因为此前没有相应的经验 刚开始的时候觉得这个技术很神秘,但在看了一些例子以后发现并不是那么复杂(不过网上的资料并不是特别详细),经过一个上午加中下午的试验 终于把该做的任务完成了 下面是一些心得和一个简单的例子 希望大虾们不要笑话我哈 一般来说 用php转换输出html页面有两种办法 引用大虾的文章

  • Mysql 5.7.18安装方法及启动MySQL服务的过程详解

    MySQL 是一个非常强大的关系型数据库.但有些初学者在安装配置的时候,遇到种种的困难,在此就不说安装过程了,说一下配置过程.在官网下载的MySQL时候,有msi格式和zip格式.Msi直接运行安装即可,zip则解压在自己喜欢的目录地址即可.在安装这两种的时候,都需要配置才能用.以下介绍主要是msi格式默认的地址:C:\Program Files\ mysql-5.7.18-win32. 一.在安装或者解压后,需要配置环境变量,过程如下:我的电脑->属性->高级系统设置->高级->

  • JavaScript数组的栈方法与队列方法详解

    数组(Array)和对象(Object)应该是JavaScript中使用最多也是最频繁的两种类型了,Array提供了很多常用的方法:栈方法.队列方法.重排序方法.操作方法.位置方法.迭代方法等等. 1.Array的栈方法 栈是一种LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除.栈中项的插入(push)和移除,只发生在一个位置--栈的顶部.ECMAScript为数组提供了push()和pop()方法,可以实现类似栈的行为.下面两图分别演示了入栈与出

随机推荐