如何详细解释HashMap在Map集合中的应用与操作?

2026-05-25 22:321阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计4777个文字,预计阅读时间需要20分钟。

如何详细解释HashMap在Map集合中的应用与操作?

目录 + HashMap + 概述 + JDK 1.8 + 之前与之后的 HashMap + HashMap 的数组,链表,红黑树之间的转换 + HashMap + 扩容机制 + HashMap + 源码 + HashMap + 的基本属性 + HashMap + 中涉及到的数据结构 + HashMap + 的 put() 方法

如何详细解释HashMap在Map集合中的应用与操作?

目录
  • HashMap 概述
  • jdk 1.8 之前与之后的 HashMap
  • HashMap 的数组,链表,红黑树之间的转换
  • HashMap 扩容机制
  • HashMap 源码
    • HashMap 的基本属性
    • HashMap 中涉及到的数据结构
    • HashMap 的 put() 方法
    • HashMap 的 get() 方法
    • HashMap 的 remove 方法
    • HashMap 的 treeifyBin() 方法
  • HashMap 如何解决 Hash 冲突
    • HashMap 存入和取出数据顺序不一致
      • 解决办法

    HashMap 概述

    HashMap 是通过 put(key,value) 存储,get(key)来获取。当传入 key 时,HashMap 会根据 key 的 hashCode() 方法计算出 hash 值,根据 hash 值将 value 保存在 bucket(桶)里。当计算出的 hash 值相同时,称之为 hash 冲突。HashMap 的做法是用链表和红黑树存储相同 hash 值的 value

    jdk 1.8 之前与之后的 HashMap

    • 在 jdk1.8 之前的 HashMap 是由数组 + 链表来实现的,数组是 HashMap 的主体。链表主要是为了解决 hash 冲突的
    • 在 jdk1.8 之后的 HashMap 是由数组 + 链表 + 红黑树来实现的,在解决 hash 冲突时有了较大的变化。当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储

    HashMap 的数组,链表,红黑树之间的转换

    • 当创建 HashMap 集合对象的时候,在 jdk1.8 之前,是在它的构造方法中创建了一个默认长度是 16 的 Entry[] table 的数组来存储键值对数据的。而从 jdk1.8开始,是在第一次调用 put 方法时创建了一个默认长度是 16 的 Node[] table 的数组来存储键值对数据的
    • 数组创建完成后,当添加一个元素(key,value)时,首先计算元素 key 的 hash 值,以此确定插入数组中的位置。但是可能存在同一 hash 值的元素已经被放在数组同一位置了,这时就添加到同一 hash 值的元素的后面,他们在数组的同一位置,这就形成了单链表,同一各链表上的 Hash 值是相同的。当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储,这样大大提高了查找的效率
    • 在转换为红黑树存储数据后,如果此时再次删除数据,当红黑树的节点数小于 6 时,那么此时的红黑树将转换为单链表结构来存储数据

    HashMap 扩容机制

    默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16 * 0.75 = 12(这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 2*16 = 32,即扩大一倍,然后重新计算每个元素在数组中的位置

    0.75 这个值称为负载因子,那么为什么负载因子为 0.75 呢? 这是通过大量实验统计得出来的,如果过小,比如 0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如 1,那么当元素满的时候才进行扩容,会使 get,put 操作的碰撞几率增加

    HashMap 源码

    HashMap 的基本属性

    public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列号 private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树;+对应的table的最小大小为64,即MIN_TREEIFY_CAPACITY ;这两个条件都满足,会链表会转红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 填充因子 final float loadFactor; }

    HashMap 中涉及到的数据结构

    链表节点(单链表)

    Node 是 HashMap 的一个内部类,实现了 Map.Entry 接口,本质上是一个单链表的数据结构。链表中的每个节点就是一个 Node 对象

    static class Node<k,v> implements Map.Entry<k,v> { final int hash; final K key; V value; Node<k,v> next; // 下一个节点 Node(int hash, K key, V value, Node<k,v> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + = + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<!--?,?--> e = (Map.Entry<!--?,?-->)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }

    红黑树节点

    红黑树比链表多了四个变量,parent 父节点、left 左节点、right 右节点、prev上一个同级节点

    static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {     TreeNode<k,v> parent;  // 父节点     TreeNode<k,v> left; // 左子树     TreeNode<k,v> right;// 右子树     TreeNode<k,v> prev; // 删除时需要取消下一个链接     boolean red;    // 颜色属性     TreeNode(int hash, K key, V val, Node<k,v> next) {         super(hash, key, val, next);     }       // 返回当前节点的根节点     final TreeNode<k,v> root() {         for (TreeNode<k,v> r = this, p;;) {             if ((p = r.parent) == null)                 return r;             r = p;         }     }          // ......省略 }

    位桶

    存储(位桶)的数组

    transient Node<K,V>[] table;

    HashMap 类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个 Node 的数组

    HashMap 的 put() 方法

    数组判断

    • 判断 tab[] 数组是否为 null 或长度为 0,如果是 null 或长度为 0;则通过 resize() 方法初始化数组,并获取长度
    • 如果单链表 Node<K,V> p == tab[i = (n - 1) & hash]) == null,就直接 put 进单链表中,说明此时并没有发生 hash 冲突
    • 如果该数组索引位置之前放入过数据,在通过 key 的 hash 值,(k = p.key) == key || (key != null && key.equals(k)) 判断该 put 的数据是否与数组索引位置数据相同;如果相同,使用 e = p 来则初始化数组 Node<K,V> e

    红黑树判断

    • 如果不相同,则判断是否是红黑树,如果是红黑树就直接插入树中

    链表判断

    • 如果不是红黑树,就遍历链表每个节点,并判断链表末尾节点是否为 null;如果为 null,则在该单链表末尾节点插入数据,并判断是否 binCount > 8 -1,为 true 的话会调用 treeifyBin(tab, hash) 方法,该方法在后面详解
    • 然后,判断该 put 的数据是否与单链表的某个节点数据相同,如果相同则跳出循环,执行下一步

    public V put(K key, V value) {     return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                    boolean evict) {     // tab表示 Node<K,V>类型的数组,p表示某一个具体的单链表 Node<K,V> 节点     Node<K,V>[] tab; Node<K,V> p; int n, i;     // 判断 table[] 是否为空,如果是空的就创建一个 table[],并获取他的长度n     if ((tab = table) == null || (n = tab.length) == 0)         n = (tab = resize()).length;         // tab[i = (n - 1) & hash] 表示数组中的某一个具体位置的数据     // 如果单链表 Node<K,V> p == tab[i = (n - 1) & hash]) == null,     // 就直接 put 进单链表中,说明此时并没有发生 Hash 冲突     if ((p = tab[i = (n - 1) & hash]) == null)         tab[i] = newNode(hash, key, value, null);     else {         // 说明索引位置已经放入过数据了,已经在单链表处产生了Hash冲突         Node<K,V> e; K k;         // 判断 put 的数据和之前的数据是否重复         if (p.hash == hash &&             // 进行 key 的 hash 值和 key 的 equals() 和 == 比较,如果都相等,则初始化节点 Node<K,V> e             ((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 {             // 如果不是红黑树,就遍历每个节点,判断单链表长度是否大于等于 7,             // 如果单链表长度大于等于 7,数组的长度小于 64 时,会优先选择扩容             // 如果单链表长度大于等于 7,数组的长度大于 64 时,才会选择单链表--->红黑树             for (int binCount = 0; ; ++binCount) {                 if ((e = p.next) == null) {                     // 采用尾插法,在单链表中插入数据                     p.next = newNode(hash, key, value, null);                     // 如果 binCount >= 8 - 1                     if (binCount >= TREEIFY_THRESHOLD - 1)                          treeifyBin(tab, hash);                         break;                 }                 // 判断索引每个元素的key是否可要插入的key相同,如果相同就直接覆盖                 if (e.hash == hash &&                     ((k = e.key) == key || (key != null && key.equals(k))))                     break;                  p = e;             }         }         if (e != null) {              // 此时说明 key 的 hash 值和 key 的 equals() 和 == 比较结果都相等             // 说明数组或者单链表中有完全相同的 key             // 只需要将 value 覆盖,并将 oldValue 返回即可             V oldValue = e.value;             if (!onlyIfAbsent || oldValue == null)                 e.value = value;                 afterNodeAccess(e);                   return oldValue;         }     }     // 说明没有key相同,因此要插入一个key-value,并记录内部结构变化次数     ++modCount;     // 判断是否扩容     if (++size > threshold)         resize();     afterNodeInsertion(evict);     return null; }

    HashMap 的 get() 方法

    首先定位键值对所在桶的位置,之后再对链表或红黑树进行查找

    • 判断表或 key 是否是 null,如果是直接返回 null key 对应的 value
    • 判断索引处第一个 key 与传入 key 是否相等,如果相等直接返回
    • 如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值
    • 如果不是树,就遍历链表查找

    public V get(Object key) {     Node<K,V> e;     return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) {     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;     // 1.如果表不是空的,并且要查找索引处有值,就判断位于第一个的key是否是要查找的key     if ((tab = table) != null && (n = tab.length) > 0 &&         (first = tab[(n - 1) & hash]) != null) {         // 1.1.检查要查找的是否是第一个节点         if (first.hash == hash && // always check first node             ((k = first.key) == key || (key != null && key.equals(k))))             return first;             // 1.2.沿着第一个节点继续查找             if ((e = first.next) != null) {                 // 1.2.1.如果是红黑树,那么调用红黑树的方法查找                 if (first instanceof TreeNode)                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);                 // 1.2.2.如果是链表,那么采用链表的方式查找                 do {                     if (e.hash == hash &&                         ((k = e.key) == key || (key != null && key.equals(k))))                         return e;             } while ((e = e.next) != null);         }     }     return null; }

    HashMap 的 remove 方法

    HashMap 的删除操作并不复杂,仅需三个步骤即可完成

    • 定位桶位置
    • 遍历链表或红黑树并找到键值相等的节点
    • 删除节点

    public V remove(Object key) {     Node<K,V> e;     return (e = removeNode(hash(key), key, null, false, true)) == null ?         null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value,                            boolean matchValue, boolean movable) {        // ------------------1. 查找到要删除的节点------------------                            // tab当前数组,n当前数组容量,p根据hash从数组上找到的节点,index p节点在数组上的位置                           Node<K,V>[] tab; Node<K,V> p; int n, index;     // 当数组存在且数组容量大于0,且p节点存在     if ((tab = table) != null && (n = tab.length) > 0 &&         (p = tab[index = (n - 1) & hash]) != null) {         Node<K,V> node = null, e; K k; V v;         // 当 p 的 hash 等于参数 hash,且 p 的 key 等于参数 key         // p节点就是当前要删除的节点,将node指向p         if (p.hash == hash &&             ((k = p.key) == key || (key != null && key.equals(k))))             node = p;         // 当p节点不是要删除的节点时,说明p节点上有红黑树或者链表         else if ((e = p.next) != null) {             // p如果是红黑树,通过getTreeNode()查找节点             if (p instanceof TreeNode)                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);             // p是链表,循环链表查询节点             else {                 do {                     if (e.hash == hash &&                         ((k = e.key) == key ||                          (key != null && key.equals(k)))) {                         node = e;                         break;                     }                     p = e;                 } while ((e = e.next) != null);             }         }         // ------------------2. 删除查找到的节点------------------         // 如果查找到的node存在且machValue为false或v等于value         if (node != null && (!matchValue || (v = node.value) == value ||                              (value != null && value.equals(v)))) {             // 如果node是TreeNode则使用removeTreeNode删除节点             if (node instanceof TreeNode)                 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);             // 如果node等于p,说明移除链表头,将node的后节点放到数组上               else if (node == p)                 tab[index] = node.next;             // 说明移除的不是链表头,根据上方的循环可得,node是p的后节点,将p的后节点指向node的后节点             else                 p.next = node.next;             // 增加修改次数,减少当前数组长度             ++modCount;             --size;             afterNodeRemoval(node);             return node;         }     }     return null; }

    HashMap 的 treeifyBin() 方法

    当桶中链表长度超过 TREEIFY_THRESHOLD(默认为 8)时,就会调用 treeifyBin() 方法进行树化操作。

    但此时并不一定会树化,因为在 treeifyBin()方法中还会判断 HashMap 的数组容量是否大于等于 64。

    如果容量大于等于 64,那么进行树化,否则优先进行扩容

    final void treeifyBin(Node<K,V>[] tab, int hash) {       int n, index; Node<K,V> e;     /*      * 如果元素数组为空 或者 数组长度小于 树结构化的最小限制      * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换      * 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)      * 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。      */     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)         resize(); // 扩容       // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了     // 根据hash值和数组长度进行取模运算后,得到链表的首节点     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); // 继续遍历链表           // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表           // 把转换后的双向链表,替换原来位置上的单向链表         if ((tab[index] = hd) != null)             hd.treeify(tab);//此处单独解析     } }

    • 如果 tab 数组为 null或 tab 的数组长度 < 64 时,调用 resize() 方法
    • 否则,会将链表转换为红黑树(是为了提高查询性能,元素越多,链表的查询性能越差)
    • 说明了链表转换为红黑树的条件:当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储

    HashMap 如何解决 Hash 冲突

    hash 冲突:在 put 多个元素时,通过 key 的 hashCode() 方法计算出的值是一样的,是同一个存储地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了 hash 冲突。当发生 hash 冲突时,会进行如下操作

    • put 的 key == 已存在的 key 的判断
    • put 的 key equals() 已存在的 key 的判断(注意 HashMap 中并没有重写 equals() 方法,这里的 equals() 方法仍然是 Object 类的方法)
    • 这里也体现了 == 与 equals() 方法的判断区别

    当上述条件判断,只要有一个返回 false 时,也就是说需要put 的 key 与已存在的 key 是不相同的,则 HashMap 会使用单链表在已有数据的后面(单链表中)插入新数据,访问的数组下标元素作为链表的头部。这种解决 Hash 冲突的方法被称为拉链法

    HashMap 存入和取出数据顺序不一致

    HashMap 遍历时,取得数据的顺序是完全随机的,这样会导致按照顺序读取的时候和存入的顺序是不一样的

    public class MapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put("2020-10-1", "李军"); map.put("2020-10-3", "李华"); map.put("2020-11-1", "李刚"); map.put("2020-10-9", "李奎"); for (String key : map.keySet()) { System.out.println(key + ":" + map.get(key)); } } } 结果: 2020-10-3 : 李华 2020-11-1 : 李刚 2020-10-1 : 李军 2020-10-9 : 李奎

    解决办法

    • 使用 LinkedHashMap
    • 使用 TreeMap:它在内部会对 Key 进行排序
    • 通过有序的 Key 获取相应的数据

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持自由互联。

    本文共计4777个文字,预计阅读时间需要20分钟。

    如何详细解释HashMap在Map集合中的应用与操作?

    目录 + HashMap + 概述 + JDK 1.8 + 之前与之后的 HashMap + HashMap 的数组,链表,红黑树之间的转换 + HashMap + 扩容机制 + HashMap + 源码 + HashMap + 的基本属性 + HashMap + 中涉及到的数据结构 + HashMap + 的 put() 方法

    如何详细解释HashMap在Map集合中的应用与操作?

    目录
    • HashMap 概述
    • jdk 1.8 之前与之后的 HashMap
    • HashMap 的数组,链表,红黑树之间的转换
    • HashMap 扩容机制
    • HashMap 源码
      • HashMap 的基本属性
      • HashMap 中涉及到的数据结构
      • HashMap 的 put() 方法
      • HashMap 的 get() 方法
      • HashMap 的 remove 方法
      • HashMap 的 treeifyBin() 方法
    • HashMap 如何解决 Hash 冲突
      • HashMap 存入和取出数据顺序不一致
        • 解决办法

      HashMap 概述

      HashMap 是通过 put(key,value) 存储,get(key)来获取。当传入 key 时,HashMap 会根据 key 的 hashCode() 方法计算出 hash 值,根据 hash 值将 value 保存在 bucket(桶)里。当计算出的 hash 值相同时,称之为 hash 冲突。HashMap 的做法是用链表和红黑树存储相同 hash 值的 value

      jdk 1.8 之前与之后的 HashMap

      • 在 jdk1.8 之前的 HashMap 是由数组 + 链表来实现的,数组是 HashMap 的主体。链表主要是为了解决 hash 冲突的
      • 在 jdk1.8 之后的 HashMap 是由数组 + 链表 + 红黑树来实现的,在解决 hash 冲突时有了较大的变化。当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储

      HashMap 的数组,链表,红黑树之间的转换

      • 当创建 HashMap 集合对象的时候,在 jdk1.8 之前,是在它的构造方法中创建了一个默认长度是 16 的 Entry[] table 的数组来存储键值对数据的。而从 jdk1.8开始,是在第一次调用 put 方法时创建了一个默认长度是 16 的 Node[] table 的数组来存储键值对数据的
      • 数组创建完成后,当添加一个元素(key,value)时,首先计算元素 key 的 hash 值,以此确定插入数组中的位置。但是可能存在同一 hash 值的元素已经被放在数组同一位置了,这时就添加到同一 hash 值的元素的后面,他们在数组的同一位置,这就形成了单链表,同一各链表上的 Hash 值是相同的。当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储,这样大大提高了查找的效率
      • 在转换为红黑树存储数据后,如果此时再次删除数据,当红黑树的节点数小于 6 时,那么此时的红黑树将转换为单链表结构来存储数据

      HashMap 扩容机制

      默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16 * 0.75 = 12(这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 2*16 = 32,即扩大一倍,然后重新计算每个元素在数组中的位置

      0.75 这个值称为负载因子,那么为什么负载因子为 0.75 呢? 这是通过大量实验统计得出来的,如果过小,比如 0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如 1,那么当元素满的时候才进行扩容,会使 get,put 操作的碰撞几率增加

      HashMap 源码

      HashMap 的基本属性

      public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列号 private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树;+对应的table的最小大小为64,即MIN_TREEIFY_CAPACITY ;这两个条件都满足,会链表会转红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 填充因子 final float loadFactor; }

      HashMap 中涉及到的数据结构

      链表节点(单链表)

      Node 是 HashMap 的一个内部类,实现了 Map.Entry 接口,本质上是一个单链表的数据结构。链表中的每个节点就是一个 Node 对象

      static class Node<k,v> implements Map.Entry<k,v> { final int hash; final K key; V value; Node<k,v> next; // 下一个节点 Node(int hash, K key, V value, Node<k,v> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + = + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<!--?,?--> e = (Map.Entry<!--?,?-->)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }

      红黑树节点

      红黑树比链表多了四个变量,parent 父节点、left 左节点、right 右节点、prev上一个同级节点

      static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {     TreeNode<k,v> parent;  // 父节点     TreeNode<k,v> left; // 左子树     TreeNode<k,v> right;// 右子树     TreeNode<k,v> prev; // 删除时需要取消下一个链接     boolean red;    // 颜色属性     TreeNode(int hash, K key, V val, Node<k,v> next) {         super(hash, key, val, next);     }       // 返回当前节点的根节点     final TreeNode<k,v> root() {         for (TreeNode<k,v> r = this, p;;) {             if ((p = r.parent) == null)                 return r;             r = p;         }     }          // ......省略 }

      位桶

      存储(位桶)的数组

      transient Node<K,V>[] table;

      HashMap 类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个 Node 的数组

      HashMap 的 put() 方法

      数组判断

      • 判断 tab[] 数组是否为 null 或长度为 0,如果是 null 或长度为 0;则通过 resize() 方法初始化数组,并获取长度
      • 如果单链表 Node<K,V> p == tab[i = (n - 1) & hash]) == null,就直接 put 进单链表中,说明此时并没有发生 hash 冲突
      • 如果该数组索引位置之前放入过数据,在通过 key 的 hash 值,(k = p.key) == key || (key != null && key.equals(k)) 判断该 put 的数据是否与数组索引位置数据相同;如果相同,使用 e = p 来则初始化数组 Node<K,V> e

      红黑树判断

      • 如果不相同,则判断是否是红黑树,如果是红黑树就直接插入树中

      链表判断

      • 如果不是红黑树,就遍历链表每个节点,并判断链表末尾节点是否为 null;如果为 null,则在该单链表末尾节点插入数据,并判断是否 binCount > 8 -1,为 true 的话会调用 treeifyBin(tab, hash) 方法,该方法在后面详解
      • 然后,判断该 put 的数据是否与单链表的某个节点数据相同,如果相同则跳出循环,执行下一步

      public V put(K key, V value) {     return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                    boolean evict) {     // tab表示 Node<K,V>类型的数组,p表示某一个具体的单链表 Node<K,V> 节点     Node<K,V>[] tab; Node<K,V> p; int n, i;     // 判断 table[] 是否为空,如果是空的就创建一个 table[],并获取他的长度n     if ((tab = table) == null || (n = tab.length) == 0)         n = (tab = resize()).length;         // tab[i = (n - 1) & hash] 表示数组中的某一个具体位置的数据     // 如果单链表 Node<K,V> p == tab[i = (n - 1) & hash]) == null,     // 就直接 put 进单链表中,说明此时并没有发生 Hash 冲突     if ((p = tab[i = (n - 1) & hash]) == null)         tab[i] = newNode(hash, key, value, null);     else {         // 说明索引位置已经放入过数据了,已经在单链表处产生了Hash冲突         Node<K,V> e; K k;         // 判断 put 的数据和之前的数据是否重复         if (p.hash == hash &&             // 进行 key 的 hash 值和 key 的 equals() 和 == 比较,如果都相等,则初始化节点 Node<K,V> e             ((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 {             // 如果不是红黑树,就遍历每个节点,判断单链表长度是否大于等于 7,             // 如果单链表长度大于等于 7,数组的长度小于 64 时,会优先选择扩容             // 如果单链表长度大于等于 7,数组的长度大于 64 时,才会选择单链表--->红黑树             for (int binCount = 0; ; ++binCount) {                 if ((e = p.next) == null) {                     // 采用尾插法,在单链表中插入数据                     p.next = newNode(hash, key, value, null);                     // 如果 binCount >= 8 - 1                     if (binCount >= TREEIFY_THRESHOLD - 1)                          treeifyBin(tab, hash);                         break;                 }                 // 判断索引每个元素的key是否可要插入的key相同,如果相同就直接覆盖                 if (e.hash == hash &&                     ((k = e.key) == key || (key != null && key.equals(k))))                     break;                  p = e;             }         }         if (e != null) {              // 此时说明 key 的 hash 值和 key 的 equals() 和 == 比较结果都相等             // 说明数组或者单链表中有完全相同的 key             // 只需要将 value 覆盖,并将 oldValue 返回即可             V oldValue = e.value;             if (!onlyIfAbsent || oldValue == null)                 e.value = value;                 afterNodeAccess(e);                   return oldValue;         }     }     // 说明没有key相同,因此要插入一个key-value,并记录内部结构变化次数     ++modCount;     // 判断是否扩容     if (++size > threshold)         resize();     afterNodeInsertion(evict);     return null; }

      HashMap 的 get() 方法

      首先定位键值对所在桶的位置,之后再对链表或红黑树进行查找

      • 判断表或 key 是否是 null,如果是直接返回 null key 对应的 value
      • 判断索引处第一个 key 与传入 key 是否相等,如果相等直接返回
      • 如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值
      • 如果不是树,就遍历链表查找

      public V get(Object key) {     Node<K,V> e;     return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) {     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;     // 1.如果表不是空的,并且要查找索引处有值,就判断位于第一个的key是否是要查找的key     if ((tab = table) != null && (n = tab.length) > 0 &&         (first = tab[(n - 1) & hash]) != null) {         // 1.1.检查要查找的是否是第一个节点         if (first.hash == hash && // always check first node             ((k = first.key) == key || (key != null && key.equals(k))))             return first;             // 1.2.沿着第一个节点继续查找             if ((e = first.next) != null) {                 // 1.2.1.如果是红黑树,那么调用红黑树的方法查找                 if (first instanceof TreeNode)                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);                 // 1.2.2.如果是链表,那么采用链表的方式查找                 do {                     if (e.hash == hash &&                         ((k = e.key) == key || (key != null && key.equals(k))))                         return e;             } while ((e = e.next) != null);         }     }     return null; }

      HashMap 的 remove 方法

      HashMap 的删除操作并不复杂,仅需三个步骤即可完成

      • 定位桶位置
      • 遍历链表或红黑树并找到键值相等的节点
      • 删除节点

      public V remove(Object key) {     Node<K,V> e;     return (e = removeNode(hash(key), key, null, false, true)) == null ?         null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value,                            boolean matchValue, boolean movable) {        // ------------------1. 查找到要删除的节点------------------                            // tab当前数组,n当前数组容量,p根据hash从数组上找到的节点,index p节点在数组上的位置                           Node<K,V>[] tab; Node<K,V> p; int n, index;     // 当数组存在且数组容量大于0,且p节点存在     if ((tab = table) != null && (n = tab.length) > 0 &&         (p = tab[index = (n - 1) & hash]) != null) {         Node<K,V> node = null, e; K k; V v;         // 当 p 的 hash 等于参数 hash,且 p 的 key 等于参数 key         // p节点就是当前要删除的节点,将node指向p         if (p.hash == hash &&             ((k = p.key) == key || (key != null && key.equals(k))))             node = p;         // 当p节点不是要删除的节点时,说明p节点上有红黑树或者链表         else if ((e = p.next) != null) {             // p如果是红黑树,通过getTreeNode()查找节点             if (p instanceof TreeNode)                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);             // p是链表,循环链表查询节点             else {                 do {                     if (e.hash == hash &&                         ((k = e.key) == key ||                          (key != null && key.equals(k)))) {                         node = e;                         break;                     }                     p = e;                 } while ((e = e.next) != null);             }         }         // ------------------2. 删除查找到的节点------------------         // 如果查找到的node存在且machValue为false或v等于value         if (node != null && (!matchValue || (v = node.value) == value ||                              (value != null && value.equals(v)))) {             // 如果node是TreeNode则使用removeTreeNode删除节点             if (node instanceof TreeNode)                 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);             // 如果node等于p,说明移除链表头,将node的后节点放到数组上               else if (node == p)                 tab[index] = node.next;             // 说明移除的不是链表头,根据上方的循环可得,node是p的后节点,将p的后节点指向node的后节点             else                 p.next = node.next;             // 增加修改次数,减少当前数组长度             ++modCount;             --size;             afterNodeRemoval(node);             return node;         }     }     return null; }

      HashMap 的 treeifyBin() 方法

      当桶中链表长度超过 TREEIFY_THRESHOLD(默认为 8)时,就会调用 treeifyBin() 方法进行树化操作。

      但此时并不一定会树化,因为在 treeifyBin()方法中还会判断 HashMap 的数组容量是否大于等于 64。

      如果容量大于等于 64,那么进行树化,否则优先进行扩容

      final void treeifyBin(Node<K,V>[] tab, int hash) {       int n, index; Node<K,V> e;     /*      * 如果元素数组为空 或者 数组长度小于 树结构化的最小限制      * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换      * 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)      * 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。      */     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)         resize(); // 扩容       // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了     // 根据hash值和数组长度进行取模运算后,得到链表的首节点     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); // 继续遍历链表           // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表           // 把转换后的双向链表,替换原来位置上的单向链表         if ((tab[index] = hd) != null)             hd.treeify(tab);//此处单独解析     } }

      • 如果 tab 数组为 null或 tab 的数组长度 < 64 时,调用 resize() 方法
      • 否则,会将链表转换为红黑树(是为了提高查询性能,元素越多,链表的查询性能越差)
      • 说明了链表转换为红黑树的条件:当链表长度大于阈值 8 时,并且数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储

      HashMap 如何解决 Hash 冲突

      hash 冲突:在 put 多个元素时,通过 key 的 hashCode() 方法计算出的值是一样的,是同一个存储地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了 hash 冲突。当发生 hash 冲突时,会进行如下操作

      • put 的 key == 已存在的 key 的判断
      • put 的 key equals() 已存在的 key 的判断(注意 HashMap 中并没有重写 equals() 方法,这里的 equals() 方法仍然是 Object 类的方法)
      • 这里也体现了 == 与 equals() 方法的判断区别

      当上述条件判断,只要有一个返回 false 时,也就是说需要put 的 key 与已存在的 key 是不相同的,则 HashMap 会使用单链表在已有数据的后面(单链表中)插入新数据,访问的数组下标元素作为链表的头部。这种解决 Hash 冲突的方法被称为拉链法

      HashMap 存入和取出数据顺序不一致

      HashMap 遍历时,取得数据的顺序是完全随机的,这样会导致按照顺序读取的时候和存入的顺序是不一样的

      public class MapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put("2020-10-1", "李军"); map.put("2020-10-3", "李华"); map.put("2020-11-1", "李刚"); map.put("2020-10-9", "李奎"); for (String key : map.keySet()) { System.out.println(key + ":" + map.get(key)); } } } 结果: 2020-10-3 : 李华 2020-11-1 : 李刚 2020-10-1 : 李军 2020-10-9 : 李奎

      解决办法

      • 使用 LinkedHashMap
      • 使用 TreeMap:它在内部会对 Key 进行排序
      • 通过有序的 Key 获取相应的数据

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持自由互联。