在分析HashMap之前,先看下图,理解一下HashMap的结构

我手画了一个图,简单描述一下HashMap的结构,数组+链表构成一个HashMap,当我们调用put方法的时候增加一个新的 key-value 的时候,HashMap会通过key的hash值和当前node数据的长度计算出来一个index值,然后在把 hash,key,value 创建一个Node对象,根据index存入Node[]数组中,当计算出来的index上已经存在了Node对象的话。就把新值存在 Node[index].next 上,就像图中的 a->aa->a1 一样,这样的情况我们称之为hash冲突

HashMap基本用法

  1. Map<String, Object> map = new HashMap<>();
  2. map.put("student", "333");//正常入数组,i=5
  3. map.put("goods", "222");//正常入数据,i=9
  4. map.put("product", "222");//正常入数据,i=2
  5. map.put("hello", "222");//正常入数据,i=11
  6. map.put("what", "222");//正常入数据,i=3
  7. map.put("fuck", "222");//正常入数据,i=7
  8. map.put("a", "222");//正常入数据,i=1
  9. map.put("b", "222");//哈希冲突,i=2,product.next
  10. map.put("c", "222");//哈希冲突,i=3,what.next
  11. map.put("d", "222");//正常入数据,i=4
  12. map.put("e", "222");//哈希冲突,i=5,student.next
  13. map.put("f", "222");//正常入数据,i=6
  14. map.put("g", "222");//哈希冲突,i=7,fuck.next

首先我们都是创建一个Map对象,然后用HashMap来实现,通过调用 put get 方法就可以实现数据存储,我们就先从构造方法开始分析

  1. public HashMap() {
  2. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  3. }

初始化负载因子为0.75,负载因子的作用是计算一个扩容阀值,当容器内数量达到阀值时,HashMap会进行一次resize,把容器大小扩大一倍,同时也会重新计算扩容阀值。扩容阀值=容器数量 * 负载因子,具体为啥是0.75别问我,自己查资料吧(其实我是不知道,我觉得这个不重要吧~)

继续看 put 方法

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }

额,也没啥可看的,继续往下看putVal方法吧

  1. transient Node<K,V>[] table;
  2. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  3. boolean evict) {
  4. Node<K,V>[] tab; Node<K,V> p; int n, i;
  5. //先判断当前容器内的哈希表是否是空的,如果table都是空的就会触发resize()扩容
  6. if ((tab = table) == null || (n = tab.length) == 0)
  7. n = (tab = resize()).length;
  8. //通过 (n - 1) & hash 计算索引,稍后单独展开计算过程
  9. if ((p = tab[i = (n - 1) & hash]) == null)
  10. //如果算出来的索引上是空的数据,直接创建Node对象存储在tab下
  11. tab[i] = newNode(hash, key, value, null);
  12. else {
  13. //如果tab[i]不为空,说明之前已经存有值了
  14. Node<K,V> e; K k;
  15. //如果key相同,则需要先把旧的 Node 对象取出来存储在e上,下边会对e做替换value的操作
  16. if (p.hash == hash &&
  17. ((k = p.key) == key || (key != null && key.equals(k))))
  18. e = p;
  19. else if (p instanceof TreeNode)
  20. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  21. else {
  22. //在这里解决hash冲突,判断当前 node[index].next 是否是空的,如果为空,就直接
  23. //创建新Node在next上,比如我贴的图上,a -> aa -> a1
  24. //大概逻辑就是a占了0索引,然后aa通过 (n - 1) & hash 得到的还是0索引
  25. //就会判断a的next节点,如果a的next节点不为空,就继续循环next节点。直到为空为止
  26. for (int binCount = 0; ; ++binCount) {
  27. if ((e = p.next) == null) {
  28. p.next = newNode(hash, key, value, null);
  29. //如果当前这个链表上数量超过8个,会直接转化为红黑树,因为红黑树查找效率
  30. //要比普通的单向链表速度快,性能好
  31. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  32. treeifyBin(tab, hash);
  33. break;
  34. }
  35. if (e.hash == hash &&
  36. ((k = e.key) == key || (key != null && key.equals(k))))
  37. break;
  38. p = e;
  39. }
  40. }
  41. //只有替换value的时候,e才不会空
  42. if (e != null) { // existing mapping for key
  43. V oldValue = e.value;
  44. if (!onlyIfAbsent || oldValue == null)
  45. e.value = value;
  46. afterNodeAccess(e);
  47. return oldValue;
  48. }
  49. }
  50. //在增加计数器
  51. ++modCount;
  52. //判断是否超过了负载,如果超过了会进行一次扩容操作
  53. if (++size > threshold)
  54. resize();
  55. afterNodeInsertion(evict);
  56. return null;
  57. }

虽然写我加了注释,但是我还是简单说一下这个的逻辑吧

1.首先判断哈希表,是否存在,不存在的时候,通过resize进行创建

2.然后在通过索引算法计算哈希表上是否存在该数据,不存在就新增node节点存储,然后方法结束

3.如果目标索引上存在数据,则需要用equals方法判断key的内容,要是判断命中,就是替换value,方法结束

4.要是key也不一样,索引一样,那么就是哈希冲突,HashMap解决哈希冲突的策略就是遍历链表,找到最后一个空节点,存储值,就像我的图一样。灵魂画手有木有,很生动的表式了HashMap的数据结构

5.最后一步就是判断是否到扩容阀值,容量达到阀值后,进行一次扩容,按照2倍的规则进行扩容,因为要遵循哈希表的长度必须是2次幂的概念

好,put 告一断落,我们继续 get

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }

get方法,恩,好,很简单。hash一下key,然后通过getNode来获取节点,然后返回value,恩。get就讲完了,哈哈。开个玩笑。我们继续看getNode

  1. final Node<K,V> getNode(int hash, Object key) {
  2. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  3. //哈希表存在的情况下,根据hash获取链表的头,也就是first对象
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (first = tab[(n - 1) & hash]) != null) {
  6. //检测第一个first是的hash和key的内容是否匹配,匹配就直接返回
  7. if (first.hash == hash && // always check first node
  8. ((k = first.key) == key || (key != null && key.equals(k))))
  9. return first;
  10. //链表的头部如果不是那就开始遍历整个链表,如果是红黑树节点,就用红黑树的方式遍历
  11. //整个链表的遍历就是通过比对hash和equals来实现
  12. if ((e = first.next) != null) {
  13. if (first instanceof TreeNode)
  14. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  15. do {
  16. if (e.hash == hash &&
  17. ((k = e.key) == key || (key != null && key.equals(k))))
  18. return e;
  19. } while ((e = e.next) != null);
  20. }
  21. }
  22. return null;
  23. }

我们在整理一下,get方法比put要简单很多,核心逻辑就是取出来索引上的节点,然后挨个匹配hash和equals,直到找出节点。

那么get方法就搞定了

再来看一下resize吧。就是HashMap的扩容机制

  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;
  3. //检测旧容器,如果旧容器是空的,就代表不需要处理旧数据
  4. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  5. //保存扩容阀值
  6. int oldThr = threshold;
  7. int newCap, newThr = 0;
  8. if (oldCap > 0) {
  9. if (oldCap >= MAXIMUM_CAPACITY) {
  10. threshold = Integer.MAX_VALUE;
  11. return oldTab;
  12. }
  13. // 对阀值进行扩容更新,左移1位代表一次2次幂
  14. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  15. oldCap >= DEFAULT_INITIAL_CAPACITY)
  16. newThr = oldThr << 1; // double threshold
  17. }
  18. else if (oldThr > 0) // initial capacity was placed in threshold
  19. newCap = oldThr;
  20. else { // zero initial threshold signifies using defaults
  21. newCap = DEFAULT_INITIAL_CAPACITY;
  22. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  23. }
  24. //如果哈希表是空的,这里会进行初始化扩容阀值,
  25. if (newThr == 0) {
  26. float ft = (float)newCap * loadFactor;
  27. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  28. (int)ft : Integer.MAX_VALUE);
  29. }
  30. threshold = newThr;
  31. @SuppressWarnings({"rawtypes","unchecked"})
  32. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  33. table = newTab;
  34. //处理旧数据,把旧数据挪到newTab内,newTab就是扩容后的新数组
  35. if (oldTab != null) {
  36. for (int j = 0; j < oldCap; ++j) {
  37. Node<K,V> e;
  38. if ((e = oldTab[j]) != null) {
  39. oldTab[j] = null;
  40. //如果当前元素无链表,直接安置元素
  41. if (e.next == null)
  42. newTab[e.hash & (newCap - 1)] = e;
  43. //红黑树处理
  44. else if (e instanceof TreeNode)
  45. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  46. else { // preserve order
  47. //对链表的索引重新计算,如果还是0,那说明索引没变化
  48. //如果hash的第5位等于1的情况下,那说明 hash & n - 1 得出来的索引已经发生变化了,变化规则就是 j + oldCap,就是索引内向后偏移16个位置
  49. Node<K,V> loHead = null, loTail = null;
  50. Node<K,V> hiHead = null, hiTail = null;
  51. Node<K,V> next;
  52. do {
  53. next = e.next;
  54. if ((e.hash & oldCap) == 0) {
  55. if (loTail == null)
  56. loHead = e;
  57. else
  58. loTail.next = e;
  59. loTail = e;
  60. }
  61. else {
  62. if (hiTail == null)
  63. hiHead = e;
  64. else
  65. hiTail.next = e;
  66. hiTail = e;
  67. }
  68. } while ((e = next) != null);
  69. if (loTail != null) {
  70. loTail.next = null;
  71. newTab[j] = loHead;
  72. }
  73. if (hiTail != null) {
  74. hiTail.next = null;
  75. newTab[j + oldCap] = hiHead;
  76. }
  77. }
  78. }
  79. }
  80. }
  81. return newTab;
  82. }

resize方法的作用就是初始化容器,以及对容器做扩容操作,扩容规则就是double

扩容完了之后还有一个重要的操作就是会对链表上的元素重新排列

(e.hash & oldCap) == 0

在讲这个公式之前,我先做个铺垫

16的二进制是 0001 0000

32的二进制是 0010 0000

64的二进制是 0100 0000

我们知道HashMap每次扩容都是左移1位,其实就是2的m+1次幂,也就是说哈希表每次扩容都是 16、32、64........n

然后我们知道HashMap内的索引是 hash & n - 1,n代表哈希表的长度,当n=16的时候,就是hash & 0000 1111,其实就是hash的后四位,当扩容n变成32的时候,就是 hash & 0001 1111,就是后五位

我为啥要说这个,因为跟上边的 (e.hash & oldCap) == 0 有关,这里其实我们也可以用

假设我们的HashMap从16扩容都了32。

其实可以用 e.hash & newCap -1 的方式来重新计算索引,然后在重排链表,但是源码作者采用的是另外一种方式(其实我觉得性能上应该一样)作者采用的是直接比对 e.hash 的第五位(16长度是后四位,32长度是后五位)进行 0 1校验,如果为0那么就可以说明 (hash & n - 1)算出来的索引没有变化,还是当前位置。要是第五位校验为1,那么这里(hash & n - 1)的公式得出来的索引就是向数据后偏移了16位。

所以作者在这里定义了两个链表,

loHead低位表头,loTail低位表尾(靠近索引0)

hiHead高位表头,hiTail高位表尾(远离索引0)

然后对链表进行拆分,如果计算出来索引没有变化,那么还让他停留在这个链表上(拼接在loTail.next上)

如果计算索引发生了变化。那么数据就要放置在高位链表上(拼接在hiTail.next)上

好了。HashMap就讲完了,可能还需要自己消化消化,反正我是消化完了。

Java基础之HashMap原理分析(put、get、resize)的更多相关文章

  1. 【Java基础】HashMap原理详解

    哈希表(hash table) 也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中Has ...

  2. Java基础之LinkedHashMap原理分析

    知识准备HashMap 我们平时用LinkedHashMap的时候,都会写下面这段 LinkedHashMap<String, Object> map = new LinkedHashMa ...

  3. JAVA常用数据结构及原理分析

    JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ...

  4. Java基础系列--HashMap(JDK1.8)

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10022092.html Java基础系列-HashMap 1.8 概述 HashMap是 ...

  5. (6)Java数据结构-- 转:JAVA常用数据结构及原理分析

    JAVA常用数据结构及原理分析  http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balab ...

  6. 原子类java.util.concurrent.atomic.*原理分析

    原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...

  7. Java NIO使用及原理分析 (四)

    在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...

  8. Java NIO使用及原理分析 (四)(转)

    在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...

  9. 支付宝app支付java后台流程、原理分析(含nei wang chuan tou)

    java版支付宝app支付流程及原理分析 本实例是基于springmvc框架编写     一.流程步骤         1.执行流程           当手机端app(就是你公司开发的app)在支付 ...

随机推荐

  1. Solon 的 PathVariable 不需注解

    相对于 Spring boot 的 path variable :Solon 的不需注解,只要变量名对上即可: //path var demo // @XMapping("e/{p_q}/{ ...

  2. JMeter软件测试工具介绍及基本安装教程

    一.工具介绍 (一)简介 Apache JMeter是Apache组织开发的基于Java的压力测试工具.用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域. 它可以用于测试 ...

  3. Linux学习笔记 一 第一章 Linux 系统简介

    Linux简介 一.UNIX与Linux发展史

  4. Enumerable 下又有新的扩展方法啦,快来一起一睹为快吧

    一:背景 1. 讲故事 前段时间将公司的一个项目从 4.5 升级到了 framework 4.8 ,编码的时候发现 Enumerable 中多了三个扩展方法: Append, Prepend, ToH ...

  5. 性能测试必备知识(10)- Linux 是怎么管理内存的?

    做性能测试的必备知识系列,可以看下面链接的文章哦 https://www.cnblogs.com/poloyy/category/1806772.html 内存映射 日常生活常说的内存是什么 比方说, ...

  6. [netty4][netty-transport]netty之nio传输层

    [netty4][netty-transport]netty之nio传输层 nio基本处理逻辑 查看这里 Selector的处理 Selector实例构建 NioEventLoop.openSelec ...

  7. AMD 5700 XT显卡装ubuntu18.04.* 驱动的问题解决(全)

    公司开发需要测试新的 AMD显卡,由于测试服务器上的显卡是英伟达的显卡所以换完后要安装相应的驱动.由于之前装机的同事装的ubuntu是18.04.5 恰巧18.04.5在amd官网上没有相匹配的驱动( ...

  8. python:**kwargs

    **kwargs接收键值对参数,即字典, dict的pop()函数内需传2个参数,第一个参数为dict内的key, 如果有该key>第二个参数为None,最后的结果就是该key对应的value. ...

  9. Linux用户和组密令大全

    本文总结了Linux添加或者删除用户和用户组时常用的一些命令和参数. 1.建用户: adduser phpq                             passwd phpq       ...

  10. Spring Security认证流程分析--练气后期

    写在前面 在前一篇文章中,我们介绍了如何配置spring security的自定义认证页面,以及前后端分离场景下如何获取spring security的CSRF Token.在这一篇文章中我们将来分析 ...