0
点赞
收藏
分享

微信扫一扫

HashMap详解

两岁时就很帅 2021-09-25 阅读 61

偶然发现这位博主的文章很干货,部分知识讲解非常细致,尤其是开讲之前的准备知识很方便小白直接理解HashMap;故复制粘贴分享好文。
原创:SoWhat1412;
链接:https://www.codenong.com/j5e9eeee0e51d4546fb2795f7/

常见问题

随机搜罗了一些常见HashMap问题,如果把下述代码都看懂了应付这些应该没问题。

预备知识

位运算知识

位运算操作是由处理器支持的底层操作,底层硬件只支持01这样的数字,因此位运算运行速度很快。尽管现代计算机处理器拥有了更长的指令流水线和更优的架构设计,使得加法和乘法运算几乎与位运算一样快,但是位运算消耗更少的资源。常用的位运算如下:

  1. 位或 |
  1. 位非 ~
  1. 位异或 ^
  1. 有符号右移 >>
  1. 无符号右移 >>>
  1. 左移

敲重点:上面的重重都是简单的只是为了引出下面的结论:

比如a%16最终的结果一定是0~15之间的数字,而a&1111正好把a除16后的余数有效的现实出来了因为如果是1 1111这样的话最前面一位其实代表的16,也就是说二进制从倒数第五位开始只要出现了1那绝对代表的是16的倍数。结论:位运算比除法运算在运行效率上更高,对一个数取余尽量用a&二进制数这样可以更好的提速。

ArrayList

我们知道ArrayList是一个数组队列,相当于动态数组。与Java中的基本数组相比,它的容量能动态增长。它具有以下几个重点。

优点:

缺点:

LinkedList

双向链表每一个节点包含三部分(data,prev,next),它不要求空间是连续的。类似于节点跟节点之间通过前后两条线串联起来的。

ArrayList和LinkedList总结:

RedBlackTree

首先你需要对二叉树有个了解,知道这是什么样子的一个数据组合方式,然后知道二叉树查找的时候缺点,可能发生数据倾斜。因此引入了平衡二叉树,平衡二叉树的左右节点深度之差不会超过1,查找方便构建麻烦,因此又出现了红黑树。红黑树是一种平衡的二叉查找树,是一种计算机科学中常用的数据结构,最典型的应用是实现数据的关联,例如map等数据结构的实现,红黑树重要特性是( 左节点 < 根节点 < 右节点) 红黑树有以下限制:

如果您对红黑树还不太了解推荐看下博主以前写的RBT

HashTable

Hash表是一种特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。这个源于Hash表设计的特殊性,它采用了==函数映射==的思想将记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。评价函数的性能关键在于==装填因子==,以及如何合理的解决哈希冲突,具体的可看博主以前写的彻底搞定哈希表

HashMap源码剖析

概述

通常具备前面一些知识点的铺垫就可以很好的开展HashMap的讲解了,既然ArrayListLinkedListRed Black Tree各有优缺点,我们能不能集百家之长实现一个综合产物呢 === >HashMap,本文所以讲解都是基于JDK8。

HashMap的组成部分:数组 + 链表 + 红黑树。HashMap的主干是一个Node数组。NodeHashMap的基本组成单元,每一个Node包含一个key-value键值对。HashMap的时间复杂读几乎可以接近O(1)(如果出现了 哈希冲突可能会波动下),并且HashMap的空间利用率一般就是在40%左右。HashMap的大致图如下:

PS:其中几个重要节点关系如下:

  1. java.util.Map.Entry 这就是个interface定义了一些比较的接口函数。

  2. java.util.HashMap.Node 就是我们HashMap中存储的基本的KV。

  3. java.util.LinkedHashMap.EnrtyEnrty这个类继承自HashMap.Node这个类,EnrtyLIinkedHashMap的一个内部类,

  4. java.util.HashMap.TreeNodeTreeNode的构造函数向上追溯继承了LinkedHashMap.Entry,而后者又继承了HashMap.Node。所以TreeNode既保有Node的属性,同时由于添加了prev这个前驱指针使得==链表==变为了==双向==。前三个节点跟第五个红黑树相关,第四个跟next跟双向链表相关。

数据存储的大致步骤有三步。

  1. 每个数据通过HashTable里的映射函数来决定将该数据放到数组的那个地方,数组初始化时候一定是2的次幂,默认16,初始化传入的任何数字都会经过tableSizeFor调整为2次幂。
  2. 如果同一个数组的地方被分配到太多数据就用链表法来解决哈希冲突。
  3. 如果同一个节点的链表数据节点个数 > TREEIFY_THRESHOLD=8且数组长度 >= MIN_TREEIFY_CAPACITY=64,则会将该链表进化位RedBlackTree,如果RedBlackTree中节点个数小于UNTREEIFY_THRESHOLD=6会退化为链表。

特别提醒:读HashMap源码之前需要知道它大致特性如下:

重要参数

静态参数
  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
  1. static final int MAXIMUM_CAPACITY = 1 << 30
  1. static final float DEFAULT_LOAD_FACTOR = 0.75f

从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。4. static final int TREEIFY_THRESHOLD = 8

  1. static final int UNTREEIFY_THRESHOLD = 6
  1. static final int MIN_TREEIFY_CAPACITY = 64
动态参数
  1. transient Node<K,V>[] table
  1. transient Set<Map.Entry<K,V>> entrySet
  1. transient int size
  1. transient int modCount
  1. int threshold
  1. final float loadFactor

四种构造方法

  1. 默认构造方法

  2. 传入初始容量大小

  3. 传入初始容量大小及负载因子

==tableSizeFor==:作用是返回大于输入参数且最小的2的整数次幂的数。比如10,则返回16。

详解如下:

综上可得,该算法让最高位的1后面的位全变为1。最后再让结果n+1,即得到了2的整数次幂的值了。现在回来看看第一条语句:

让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,这种二进制方法的效率非常高。

  1. 构造函数传入一个map 使用默认的负载因子,然后根据当前map的大小来反推需要的threshold,同时还可能会涉到resize,然后住个put到 容器中。

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">
</figcaption>

Hash值

无论我们put数据还是get数据都要先获得该数据在这个哈希表中对应的位置。比如put数据,它的流程分为2步。

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">
</figcaption>

同时JDK8跟JDK7的扰动目的一样,不过复杂程度不一样

1. get

相对来说很简单,为方便理解先说下代码大致流程思路。

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">
</figcaption>

1.1 getNode

宏观查找函数细节:

1.3 getTreeNode

红黑树查找节点细节:

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">
</figcaption>

1.4 ComparableClassFor:

查询该key是否实现了Comparable接口。

1.5 compareComparables:

既然实现了Comparable接口就用该实现进行对比判断如何何去何从。

2. put流程

跟随源码梳理下put操作的大致流程。

数据插入的时候大致流程如下:

  1. 对数据进行Hash值计算。
  2. 将数据插入前先查看下当前table的状态,如果table是空需要调用resize来进行初始化。
  3. 通过位运算获得key的目标位置。并判断当前位置情况。
  4. 如果当前位置为空则直接进行放置,如果跟当前key一直则进行覆盖。
  5. 如果当前有数据则看当前数据类型是否是红黑树,是的话需要调用putTreeVal
  6. 否则就认为是个链表,然后循环的查找进行尾部==插入==。同时还要考虑当前链表转红黑树。

7. 对找到的旧节点e进行判断

  1. 数据最终添加完毕后要对对修改后的变量modCount加1,同时看最新的总的节点数是否需要扩容了,如果是就扩容。

2 put

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">
</figcaption>

2.1 putTreeVal

  1. 先找到根节点,然后判断是从左边找还是右边找key。
  2. 找到了则直接返回找到的节点。
  3. 没找到则新建节点将该新建节点放到适当的位置,同时考虑红黑树跟双向链表的节点插入情况。

2.2 treeifyBin

主要功能是根据参数的阈值范围绝对是否将链表转化为红黑树,然后首先将单项链表转化为双向链表,再调用treeify以头节点为根节点构建红黑树。

2.3 treeify

双向链表跟红黑树创建,主要步骤分3步。

  1. 从该双向链表中找第一个节点为root节点。
  2. 每一个双向链表节点都在root节点为根都二叉树中找位置然后将该数据插入到红黑树中,同时要注意balance
  3. 最终要注意将根节点跟当前table[i]对应好。

2.4 moveRootToFront

确保将root节点挪动到table[first]上,如果红黑树构建成功而没成功执行这个任务会导致tablle[first]对应的节点不是红黑树的root节点。正常执行的时候主要步骤分2步。

  1. 找到跟节点然后将root节点放到跟节点,至此关于红黑树到操作搞定。
  2. 原来链表头是first节点,现在将可能是中间节点的root节点挪到first节点前面。

其中 checkInvariants函数的作用:校验TreeNode对象是否满足红黑树和双链表的特性。因为并发情况下会发生很多异常。

3 resize

链表形式的重新划分解释如下:注意:不是(e.hash & (oldCap-1))而是(e.hash & oldCap), 后一个得到的是 元素的在数组中的位置是否需要移动,示例如下

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;"></figcaption>

3.1 split

扩容后如何处理原来一个table[i]上的红黑树,代码的整体思路跟处理链表的时候差不多,只要理解节点关系保存红黑树的时候也保存了双向链表就OK了。

4. find

函数功能就是以指定的一个节点为根节点,根据指定的keyvalue进行查找。

  1. 直接从右节点递归查找下。
  2. 否则就从左边查找。

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">
</figcaption>

4.1 tieBreakOrder

对两个对象进行比较,一定能比出个高低。

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">
</figcaption>

4. remove

函数入口而已:

4.1 removeNode

removeNode无非就是查看table[i]是否存在,然后是否在首节点上,是否在红黑树上,是否在链表上。这几种情况,找到了则直接删除,同时注意平衡性。

4.2 removeTreeNode

该函数的 目的就是移除调用此方法的节点,也就是该方法中的this节点。移除包括链表的处理和红黑树的处理。可以看以前写过的RBT,删除的时候思路大致是一样的,这里大致分为3步骤。

4.3 untreeify

红黑树退化成链表

4.4 balanceDeletion

关于这个问题可以直接看博主以前写的红黑叔添加跟删除RBT

JDK7死环问题

JDK7对旧table数据重定位到新table的函数transfer如下,其中重点关注部分以标出。

  1. 头插法正常情况下:

  2. 并发情况下 线程1只执行了Entry<K,V> next = e.next就被挂起了,而线程2正常执行完毕,结果图如下:

    线程1接着下面继续执行:

    通过逐步分析跟绘图可以知道 会有环产生。
    HashIterator 的 remove 方法
    7vs8
    7中找Hash用了4次,8中只用了1次。
    7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树
    7中是头插法,多线程容易造成环,8中是尾插法。
    7的扩容是全部数据重新定位,8中是位置不变+ 移动旧size大小来实现更好些。
    7是先判断是否要扩容再插入,8中是先插入再看是否要扩容。
    HashMap不管78都是现场不安全的,多线程情况下记得用ConcurrentHashmap。ConcurrentHashmap下篇文章说。

举报

相关推荐

0 条评论