• JDK1.8之前与之后 HashMap底层实现原理的差别


    hashmap的概述

    • HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。
    • HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap中的映射不是有序的。
    • jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。
    • jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64时,此时此索引位置上的所有数据改为使用红黑树存储。

    HashMap 特点:

    • 存储无序;
    • 键和值位置都可以是 null,但是键位置只能存在一个 null;
    • 键位置是唯一的,是由底层的数据结构控制的;
    • jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树;
    • 阈值(边界值)> 8 并且桶位数(数组长度)大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询;

    HashMap存储数据的过程

       测试代码

    1. public static void main(String[] args) {
    2. HashMap map=new HashMap();
    3. map.put("student1","萧炎");
    4. map.put("student2","林动");
    5. map.put("student3","牧尘");
    6. map.put("student4","古尘沙");
    7. System.out.println(map);
    8. }

       结果

    执行流程分析

    • 首先程序读到HashMap map=new HashMap();的时候,并不会马上创建一个数组,而是在我们第一次使用hashmap自己的put方法的时候,创建一个长度为16的数组,也叫作桶Node[]table ,这个用来存储数据
    • 当我们需要存入一个数据,比方说(key=a,value=3),首先会先调用重写的String的hashcode方法,计算出对应的hash数值,然后根据数组的长度和某种算法,找到这组数据应该对应的桶位数,就是数组的下标,例如0,1,2,3,4,5…然后查看这个桶里面是否有存其他的数据,如果没有,那么就直接把数据存入桶中,我们加入这一次存在‘3’这个位置

    • 当我们又再一次的调用put方法,存入(key=b,value=4)的时候,假设这次算出来的又是存在三号位,这个时候,三号位已经有一个数据了,这个时候会判断两个数据的hash值是否相同,如果不相同,那我们这个时候就会在这个桶下生成一个链表,用来存储数据,这个叫做拉链法
    • 如果相同的话则会对两个数据进行一次判断
    • 数据相同:直接覆盖
    • 数据不同:从该桶位的链表开始,一直往下比,直到出现不同的时候,便存在不同的地方的下一个位置,如果这个时候链表长度超过了8,那么链表就会转化成红黑树
    • 在不断的添加新数据的时候,如果某一时刻超过了阈值,并且那个时候要存入数据的地方刚好不为空,那么,我们就要扩容了,每次扩容都是在原来的基础上,扩大2倍,原因后面会讲。

    jdk1.8 中引入红黑树的进一步原因

    1. jdk1.8 以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。
    2. 针对这种情况,jdk1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

    HashMap继承体系

    从继承体系可以看出:

    • HashMap 实现了Cloneable接口,可以被克隆。
    • HashMap 实现了Serializable接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。
    • HashMap 继承了AbstractMap,父类提供了 Map 实现接口,具有Map的所有功能,以最大限度地减少实现此接口所需的工作

    在Java中,HashMap的实现采用了(数组 + 链表 + 红黑树)的复杂结构,数组的一个元素又称作桶。
    在添加元素时,会根据hash值算出元素在数组中的位置,如果该位置没有元素,则直接把元素放置在此处,如果该位置有元素了,则把元素以链表的形式放置在链表的尾部。
    当一个链表的元素个数达到一定的数量(且数组的长度达到一定的长度)后,则把链表转化为红黑树,从而提高效率。
    数组的查询效率为O(1),链表的查询效率是O(k),红黑树的查询效率是O(log k),k为桶中的元素个数,所以当元素数量非常多的时候,转化为红黑树能极大地提高效率。

    HashMap基本属性与常量

    1. /*
    2. * 序列化版本号
    3. */
    4. private static final long serialVersionUID = 362498820763181265L;
    5. /**
    6. * HashMap的初始化容量(必须是 2 的 n 次幂)默认的初始容量为16
    7. */
    8. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    9. /**
    10. * 最大的容量为2的30次方
    11. */
    12. static final int MAXIMUM_CAPACITY = 1 << 30;
    13. /**
    14. * 默认的装载因子
    15. */
    16. static final float DEFAULT_LOAD_FACTOR = 0.75f;
    17. /**
    18. * 树化阈值,当一个桶中的元素个数大于等于8时进行树化
    19. */
    20. static final int TREEIFY_THRESHOLD = 8;
    21. /**
    22. * 树降级为链表的阈值,当一个桶中的元素个数小于等于6时把树转化为链表
    23. */
    24. static final int UNTREEIFY_THRESHOLD = 6;
    25. /**
    26. * 当桶的个数达到64的时候才进行树化
    27. */
    28. static final int MIN_TREEIFY_CAPACITY = 64;
    29. /**
    30. * Node数组,又叫作桶(bucket)
    31. */
    32. transient Node[] table;
    33. /**
    34. * 作为entrySet()的缓存
    35. */
    36. transient Set> entrySet;
    37. /**
    38. * 元素的数量
    39. */
    40. transient int size;
    41. /**
    42. * 修改次数,用于在迭代的时候执行快速失败策略
    43. */
    44. transient int modCount;
    45. /**
    46. * 当桶的使用数量达到多少时进行扩容,threshold = capacity * loadFactor
    47. */
    48. int threshold;
    49. /**
    50. * 装载因子
    51. */
    52. final float loadFactor;
    • 容量:容量为数组的长度,亦即桶的个数,默认为16 ,最大为2的30次方,当容量达到64时才可以树化。
    • 装载因子:装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。
    • 树化:树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。

    Hashmap属性解释

       DEFAULT_INITIAL_CAPACITY   ----   集合的初始化容量(必须是 2 的 n 次幂):

    1. // 默认的初始容量是16    1 << 4 相当于 1*2的4次方
    2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    •    HashMap 构造方法可以指定集合的初始化容量大小,根据上述讲解我们已经知道,当向 HashMap 中添加一个元素的时候,需要根据 key 的 hash 值,去确定其在数组中的具体位置。HashMap 为了存取高效,减少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现的关键就在把数据存到哪个链表中的算法。
    • 这个算法实际就是取模,hash % length,而计算机中直接求余效率不如位移运算。所以源码中做了优化,使用 hash & (length - 1),而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 是2 的 n 次幂。

    举例解释下上面说的,例如,数组长度为 8 的时候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,桶的位置是(数组索引)3和2,不同位置上,不碰撞。
    再来看一个数组长度(桶位数)不是2的n次幂的情况:

    数组长度为9 hash值为3

    00000011 3
    00001000 8
    ————————
    00000000 0

    数组长度为9 hash值为5

    00000101 5
    00001000 8
    ————————
    00000000 0

    数组长度为9 hash值为6

    00000101 5
    00001000 8
    ————————
    00000000 0

    通过上面举例,发现, 如果数组长度不是2的次幂,通过与运算就容易得到重复的hash值,这会大大增加哈希碰撞的概率,导致性能下降。

    因此我们可以得出用2的次幂原因:

    • 当我们根据key的hash确定其在数组的位置时,如果n为2的幂次方,可以保证数据的均匀插入,如果n不是2的幂次方,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大hash冲突。
    • 另一方面,一般我们可能会想通过%求余来确定位置,这样也可以,只不过性能不如&运算。而且当n是2的幂次方时: hash & (length1) == hash % length
    • 因此,HashMap容量为2次幂的原因,就是为了数据的的均匀分布,减少hash冲突,毕竟hash冲突越大,代表数组中一个链的长度越大,这样的话会降低hashmap的性能

       了解了为什么系统初始化hashmap时,必须为2的次幂。可是当用户创建hashmap对象,传入的数组长度不是2的次幂,怎么办呢?于是我查看了源代码,如下:

    1. static final int tableSizeFor(int cap) {
    2. int n = cap - 1;
    3. n |= n >>> 1;
    4. n |= n >>> 2;
    5. n |= n >>> 4;
    6. n |= n >>> 8;
    7. n |= n >>> 16;
    8. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    9. }

    这里HashMap会采用一个tableSizeFor()方法,通过这个方法,它会把数组长度设置成最接近用户输入数组长度数量的2的次幂的值。

    我们分析下源代码:

      int n = cap - 1;为什么要减去1呢?

     这是为了防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂,又没有这个减 1 操作,则执行完后面的几条无符号操作之后,返回的 capacity 将是这个 cap 的 2 倍(后面还会再举个例子讲这个)。

    最后为什么有个 n + 1 的操作呢?

    如果 n 这时为 0 了(经过了cap - 1后),则经过后面的几次无符号右移依然是 0,返回0是肯定不行的,所以最后返回n+1最终得到的 capacity 是1。
    注意:容量最大也就是 32bit 的正数,因此最后 n |= n >>> 16;最多也就 32 个 1(但是这已经是负数了,在执行 tableSizeFor 之前,对 initialCapacity 做了判断,如果大于MAXIMUM_CAPACITY(2 ^ 30),则取 MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY,会执行位移操作。所以这里面的位移操作之后,最大 30 个 1,不会大于等于 MAXIMUM_CAPACITY。30 个 1,加 1 后得 2 ^ 30)。  

        说着不好理解,画图说明:

    上面说到了 数组长度,下面说红黑树,当桶(bucket)上的结点数大于这个值时会转为红黑树

    1. // 当桶(bucket)上的结点数大于这个值时会转为红黑树
    2. static final int TREEIFY_THRESHOLD = 8;

    当链表长度低于6会从红黑树转化成链表

    1. // 当桶(bucket)上的结点数小于这个值,树转为链表
    2. static final int UNTREEIFY_THRESHOLD = 6;

    MIN_TREEIFY_CAPACITY ----  当 Map 里面的数量超过这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD(8)

    1. // 桶中结构转化为红黑树对应的数组长度最小的值
    2. static final int MIN_TREEIFY_CAPACITY = 64;

    table  ----它是存储元素的数组 

    1. // 存储元素的数组
    2. transient Node[] table;

    在 jdk1.8 中我们了解到 HashMap 是由数组加链表加红黑树来组成的结构,其中 table 就是 HashMap 中的数组,jdk8 之前数组类型是 Entry 类型。从 jdk1.8 之后是 Node 类型。只是换了个名字,都实现了一样的接口:Map.Entry。负责存储键值对数据的。
     

    entrySet  --- 用来放缓存

    1. // 存放具体元素的集合
    2. transient Set> entrySet;

    size ---- HashMap 中存放元素的个数

    1. // 存放元素的个数,注意这个不等于数组的长度
    2.  transient int size;

    size 为是记录HashMap 中 K-V 的实时数量,不是数组 table 的长度。

    modCount --- 用来记录 HashMap 的修改次数

    1. // 每次扩容和更改 map 结构的计数器
    2. transient int modCount;

    threshold --- 用来调整大小下一个容量的值计算方式为(容量*负载因子)

    1. // 临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容
    2. int threshold;

    loadFactor --- 哈希表的负载因子

    1. // 负载因子
    2. final float loadFactor;// 0.75f

    说明:

    • oadFactor 是用来衡量 HashMap 满的程度,表示HashMap的疏密程度,影响 hash 操作到同一个数组位置的概率,计算HashMap 的实时负载因子的方法为:size/capacity,而不是占用桶的数量去除以 capacity。capacity是桶的数量,也就是 table 的长度 length。
    • loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f是官方给出的一个比较好的临界值。
    • 当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap
    • 太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建HashMap 集合对象时指定初始容量来尽量避免。

    HashMap扩容机制

       

    1. /**
    2. * Initializes or doubles table size. If null, allocates in
    3. * accord with initial capacity target held in field threshold.
    4. * Otherwise, because we are using power-of-two expansion, the
    5. * elements from each bin must either stay at same index, or move
    6. * with a power of two offset in the new table.
    7. *
    8. * @return the table
    9. */
    10. final Node[] resize() {
    11. //把旧的table 赋值个一个变量
    12. Node[] oldTab = table;
    13. //获取旧的tabel的长度
    14. int oldCap = (oldTab == null) ? 0 : oldTab.length;
    15. // 旧的阈值
    16. int oldThr = threshold;
    17. int newCap, newThr = 0;
    18. if (oldCap > 0) {
    19. //判断数组的长度是否大约等于最大值
    20. if (oldCap >= MAXIMUM_CAPACITY) {
    21. //如果数组的长度达到了最大值,那么就不在进行扩容,直接返回,不管hash冲突
    22. threshold = Integer.MAX_VALUE;
    23. return oldTab;
    24. //把旧的数组长度左移一位(也就是乘以2),然后判断是否小于最大值,
    25. // 并且判断旧的数组长度是否大于等于默认的长度16
    26. }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    27. oldCap >= DEFAULT_INITIAL_CAPACITY)
    28. //如果条件成立就把旧的阈值左移一位复制给新的阈值
    29. newThr = oldThr << 1; // double threshold
    30. }//如果就的数组长度小于0并且旧的阈值大于0
    31. else if (oldThr > 0) // initial capacity was placed in threshold
    32. //就把旧的阈值赋值给新的数组长度(初始化新的数组长度)
    33. newCap = oldThr;
    34. else { // zero initial threshold signifies using defaults
    35. //使用默认值
    36. newCap = DEFAULT_INITIAL_CAPACITY;
    37. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    38. }
    39. //如果新的阈值等于0
    40. if (newThr == 0) {
    41. //那么就重新计算新的阈值上限
    42. float ft = (float)newCap * loadFactor;
    43. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    44. (int)ft : Integer.MAX_VALUE);
    45. }
    46. threshold = newThr;
    47. @SuppressWarnings({"rawtypes","unchecked"})
    48. Node[] newTab = (Node[])new Node[newCap];
    49. table = newTab;
    50. //已完成新的数组扩容,开始把就的数据移动到新的数组中,通过循环遍历
    51. if (oldTab != null) {
    52. for (int j = 0; j < oldCap; ++j) {
    53. Node e;
    54. if ((e = oldTab[j]) != null) {
    55. oldTab[j] = null;
    56. //如果没有子元素那么说明是下面不是一个链表,直接通过
    57. // hash&(新的数组长度-1)计算出新的位置,把就的数据放入新的位置
    58. if (e.next == null)
    59. newTab[e.hash & (newCap - 1)] = e;
    60. //如果该节点为红黑树结构,就进行树的操作
    61. else if (e instanceof TreeNode)
    62. ((TreeNode)e).split(this, newTab, j, oldCap);
    63. else { // preserve order
    64. /* 有多个数据并且不是树那么该节点上放的是链表,这里是java1.8很精妙的地方,如果
    65. e.hash& 旧的数组长度 如果等于0, 那么该数据的位置没有发生变化,还在原来的索引位置上,
    66. 如果不等于0 , 那么就在该值就在 (原来的索引位置+旧的数组长度)的位置上,这里重新创建了
    67. 两个节点 , 在原来位置上的放入loHead中,在新的位置上的放入hiHead 中,最后把这两组数据
    68. 放入新的数组中即可。(这里的精妙之处是不用重新计算每一个数据的hash,就可以把旧的数据
    69. 放入新的数组中去)*/
    70. Node loHead = null, loTail = null;
    71. Node hiHead = null, hiTail = null;
    72. Node next;
    73. do {
    74. next = e.next;
    75. if ((e.hash & oldCap) == 0) {
    76. if (loTail == null)
    77. loHead = e;
    78. else
    79. loTail.next = e;
    80. loTail = e;
    81. }
    82. else {
    83. if (hiTail == null)
    84. hiHead = e;
    85. else
    86. hiTail.next = e;
    87. hiTail = e;
    88. }
    89. } while ((e = next) != null);
    90. if (loTail != null) {
    91. loTail.next = null;
    92. //这里把在原来索引位置上的放入新的数组中去
    93. newTab[j] = loHead;
    94. }
    95. if (hiTail != null) {
    96. hiTail.next = null;
    97. //这里把在原来的索引位置+旧的数组长度)的位置上,放入新的数组中去
    98. newTab[j + oldCap] = hiHead;
    99. }
    100. }
    101. }
    102. }
    103. }
    104. return newTab;
    105. }

     Node内部类

         Node是一个典型的单链表节点,其中,hash用来存储key计算得来的hash值。

    1. static class Node implements Map.Entry {
    2. final int hash;// hash用来存储key计算得来的hash值
    3. final K key;// 键
    4. V value;// 值
    5. Node next;// 下一个node节点
    6. Node(int hash, K key, V value, Node next) {
    7. this.hash = hash;
    8. this.key = key;
    9. this.value = value;
    10. this.next = next;
    11. }
    12. public final K getKey() { return key; }
    13. public final V getValue() { return value; }
    14. public final String toString() { return key + "=" + value; }
    15. public final int hashCode() {// 调用底层c++ 返回Key/Value的哈希码值,如果此对象为null,则返回0
    16. return Objects.hashCode(key) ^ Objects.hashCode(value);// 将Key/Vaule
    17. }
    18. public final V setValue(V newValue) {
    19. V oldValue = value;
    20. value = newValue;
    21. return oldValue;
    22. }
    23. public final boolean equals(Object o) {
    24. if (o == this)
    25. return true;
    26. if (o instanceof Map.Entry) {
    27. Map.Entry e = (Map.Entry)o;
    28. if (Objects.equals(key, e.getKey()) &&
    29. Objects.equals(value, e.getValue()))
    30. return true;
    31. }
    32. return false;
    33. }
    34. }

      hashmap的构造函数

    1. // 构造一个映射关系与指定 Map 相同的新 HashMap。
    2. public HashMap(Map m) {
    3. // 负载因子loadFactor变为默认的负载因子0.75
    4. this.loadFactor = DEFAULT_LOAD_FACTOR;
    5. putMapEntries(m, false);
    6. }

    最后调用了 putMapEntries(),来看一下方法实现:

    1. final void putMapEntries(Map m, boolean evict) {
    2. //获取参数集合的长度
    3. int s = m.size();
    4. if (s > 0) {//判断参数集合的长度是否大于0
    5. if (table == null) { // 判断table是否已经初始化
    6. // 未初始化,s为m的实际元素个数
    7. float ft = ((float)s / loadFactor) + 1.0F;// 得到新的扩容阈值
    8. int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);// 新的扩容阈值float自动向下转型为int
    9. // 计算得到的t大于阈值,则初始化阈值,将其变为符合要求的2的n次幂数
    10. if (t > threshold)
    11. threshold = tableSizeFor(t);
    12. }
    13. // 如果table已初始化过了,并且m元素个数大于阈值,进行扩容处理
    14. else if (s > threshold)
    15. resize();
    16. // 将m中的所有元素添加至HashMap中
    17. for (Map.Entryextends K, ? extends V> e : m.entrySet()) {
    18. K key = e.getKey();
    19. V value = e.getValue();
    20. // 得到的key 和 value 放入 hashmap
    21. putVal(hash(key), key, value, false, evict);
    22. }
    23. }
    24. }

    看了上面源码,可能会对这行代码有译文 float ft = ((float)s / loadFactor) + 1.0F; 这一行代码中为什么要加 1.0F ?

       (float)s/loadFactor 的结果是小数,加 1.0F 与 (int)ft 相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少 resize 的调用次数(为了效率,应当尽量减少扩容的次数)。所以 + 1.0F 是为了获取更大的容量。
    例如:原来集合的元素个数是 6 个,那么 6/0.75 是8,由于8是 2 的n次幂,那么
    if (t > threshold) threshold = tableSizeFor(t);执行过后,新的数组大小就是 8 了。然后原来数组的数据就会存储到长度是 8 的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能降低了,而如果 +1 呢,数组长度直接变为16了,这样可以减少数组的扩容。

  • 相关阅读:
    c# entity freamwork 判断是否存在
    2024-3-13-C++day3作业
    IJCAI2023【基于双曲空间探索的非独立同分布联邦学习】
    基于SSM的网上手机商城
    Linux中每当执行‘mount’命令(或其他命令)时,自动激活执行脚本:输入密码,才可以执行mount
    电阻理论基础
    Coze+Discord:打造你的免费AI助手(教您如何免费使用GPT-4o/Gemini等最新最强的大模型/Discord如何正确连接Coze)
    代码随想录——分割回文串 II
    Nginx【1】浅聊代理
    第四代可燃气体监测仪:可燃气体监测仪效果有哪些?
  • 原文地址:https://blog.csdn.net/qq_51901495/article/details/126307662