HashMap底层原理解析(一)

接触过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 1sttreeifyBin(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 keyV 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 或 数组长度小于 64if (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>();// 利用循环进行putfor (int i = 0; i < n2; i++) {String temp = HashTest.randomString();map.put(temp, i);list.add(temp);}long time = 0; // 总耗费时间// 利用循环getfor (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的源代码,每一行都很有意思,都值得花时间去钻研、推敲。
今天的分享到这就告一段落咯,之后还会继续更新,欢迎大家继续关注。也欢迎大家前来打脸!
参考:

原理分析
HashMap为什么初始容量为2的次幂
红黑树解析
put方法了解

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

  1. java map原理_Java HashMap底层原理分析

    前两天面试的时候,被面试官问到HashMap底层原理,之前只会用,底层实现完全没看过,这两天补了补功课,写篇文章记录一下,好记性不如烂笔头啊,毕竟这年头脑子它记不住东西了哈哈哈.好了,言归正传,今天我 ...

  2. 深度解剖HashMap底层原理

    HashMap底层原理 写在前面 JDK1.7版本--HashMap java.1.7源码分析 new一个HashMap实例的存储流程图如下: API常用方法 API中重要的变量 第一步:申明一个Ha ...

  3. 我向面试官讲解了hashmap底层原理,他对我竖起了大拇指

    前言: 正值金九银十的黄金招聘期,大家都准备好了吗?HashMap是程序员面试必问的一个知识点,其内部的基本实现原理是每一位面试者都应该掌握的,只有真正地掌握了 HashMap的内部实现原理,面对面试 ...

  4. HashMap底层原理分析(put、get方法)

    1.HashMap底层原理分析(put.get方法) HashMap底层是通过数组加链表的结构来实现的.HashMap通过计算key的hashCode来计算hash值,只要hashCode一样,那ha ...

  5. 面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

    来自:烟雨星空 前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希 ...

  6. Java集合—HashMap底层原理

    原文链接:最通俗易懂搞定HashMap的底层原理 HashMap的底层原理面试必考题.为什么面试官如此青睐这道题?HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好o ...

  7. 查询已有链表的hashmap_面试官再问你 HashMap 底层原理,就把这篇文章甩给他看...

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

  8. HashMap 底层原理

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

  9. HashMap底层原理全解析

    作为面试中的高频题目,我相信每一个java程序员都有必要搞懂HashMap的底层原理和实现细节,废话不多说直接开撸. 首先简单说一下HashMap的实现原理: 首先有一个Node<k,v> ...

最新文章

  1. 用7*7的卷积核分类9*9的图片到底应该用几个卷积核?55个
  2. php隐含值传递,php – jQuery更新隐藏的输入值,但不传递给POST变量
  3. ReactiveStream01
  4. 树的同构模板题(法1.最小表示法+法2.树哈希)
  5. mysql 慢日志报警_一则MySQL慢日志监控误报的问题分析
  6. 许家印砸1000亿布局AI、量子计算等领域,但在科技圈只能算轻壕
  7. jQuery Mobile中弹窗popup的data-*选项
  8. oracle优先顺序取值,oracle取值函数
  9. (第一周)2018091-2 博客作业
  10. scala和java集合的区别_Scala中Array和List的区别
  11. python网络刷学时_python实践—网络刷博器
  12. win32API中文参考手册
  13. 职业生涯规划访谈记录关于计算机专业,计算机专业职业生涯规划书
  14. 6个Web前端值得收藏很实用的菜单模板(下)
  15. torch.nn.Embedding(num_embeddings, embedding_dim)的理解
  16. python立方根求解_计算python中的立方根
  17. 1_22_python基础学习_0518
  18. 用IE点击html页面用谷歌打开,如何在电脑中使用谷歌浏览器打开不兼容的网页
  19. 74LS139改3―8线译码器_数字译码器
  20. carsim自带4ws模型_carsim软件介绍

热门文章

  1. ipad上的人体模型_我拥有哪种iPad模型?
  2. android -- 蓝牙 bluetooth解读
  3. [HNOI2003]激光炸弹(二维前缀和+大坑点)
  4. vite使用vite-aliases插件配置路径别名
  5. JAVA每日学习 Day31---抽象类和接口的含义、共性、区别
  6. 水溶Cy7/Cy3/Cy5-SE染料,水溶性CY7活化酯,CAS号:477908-53-5
  7. python scapy 抓包_Python3下基于Scapy库完成网卡抓包解析
  8. 全闪存存储 NetApp AFF A 系列 ——引领闪存的未来发展
  9. 离线数仓(10):ODS层实现之业务数据核对
  10. 鼠标图标怎么自定义_苹果ios14怎么自定义图标 图标位置自由排列换风格教程