背过不怕面试问 HashMap!

Java 8 之前:

底层实现是数组 + 链表,主要成员变量包括:存储数据的 table 数组、键值对数量 size、加载因子 loadFactor。

table 数组用于记录 HashMap 的所有数据,它的每一个下标都对应一条链表,所有哈希冲突的数据都会被存放到同一条链表中,Entry 是链表的节点元素,包含四个成员变量:键 key、值 value、执行下一个节点的指针 next 和 元素的散列值 hash。

在 HashMap 中数据都是以键值对的形式存在的,键对应的 hash 值将会作为其在数组里的下标,如果两个元素 key 的 hash 值一样,就会发送哈希冲突,被放到同一个下标中的链表上,为了使 HashMap 的查询效率尽可能高,应该使键的 hash 值尽可能分散。

HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。

1 put 方法:添加元素

① 如果 key 为 null 值,直接存入 table[0]。

② 如果 key 不为 null 值,先计算 key 对应的散列值。

③ 调用 indexFor 方法根据 key 的散列值和数组的长度计算元素存放的下标 i。

④ 遍历 table[i] 对应的链表,如果 key 已经存在,就更新其 value 值然后返回旧的 value 值。

⑤ 如果 key 不存在,就将 modCount 的值加 1,使用 addEntry 方法增加一个节点,并返回 null 值。

2 hash 方法:计算元素 key 对应的散列值

① 处理 String 类型的数据时,直接调用对应方法来获取最终的hash值。

② 处理其他类型数据时,提供一个相对于 HashMap 实例唯一不变的随机值 hashSeed 作为计算的初始量。

③ 执行异或和无符号右移操作使 hash 值更加离散,减小哈希冲突的概率。

3 indexFor 方法:计算元素下标

直接将 hash 值和数组长度 - 1 进行与操作并返回,保证计算后的结果不会超过 table 数组的长度范围。

4 resize 方法:根据newCapacity 来确定新的扩容阈值 threshold

① 如果当前容量已经达到了最大容量,就将阈值设置为 Integer 的最大值,之后扩容就不会再触发。

② 创建一个新的容量为 newCapacity 的 Entry 数组,并调用 transfer 方法将旧数组的元素转移到新数组.

③ 将阈值设为(newCapacity 和加载因子 loadFactor 的积)和(最大容量 + 1 )的较小值。

transfer:转移旧数组到新数组

① 遍历旧数组的所有元素,调用 rehash 方法判断是否需要哈希重构,如果需要就重新计算元素 key 的散列值。

② 调用 indexFor 方法根据 key 的散列值和数组的长度计算元素存放的下标 i,将旧数组的元素转移到新的数组。

5 get 方法:根据 key 获取元素的 value 值

① 如果 key 为 null 值,调用 getForNullKey 方法,如果 size 为 0 表示链表为空,返回 null 值。如果 size 不为 0,说明存在链表,遍历 table[0] 的链表,如果找到了 key 为 null 的节点则返回其 value 值,否则返回 null 值。

② 调用 getEntry 方法,如果 size 为 0 表示链表为空,返回 null 值。如果 size 不为 0,首先计算 key 的散列值,然后遍历该链表的所有节点,如果节点的 key 值和 hash 值都和要查找的元素相同则返回其 Entry 节点。

③ 如果找到了对应的 Entry 节点,使用 getValue 方法获取其 value 值并返回,否则返回 null 值。

Java 8 之后:

使用的是数组 + 链表/红黑树的形式,table 数组的元素数据类型换成了 Entry 的静态实现类 Node。

1 put 方法:添加元素

① 调用 putVal 方法添加元素。

② 如果 table 为空或没有元素时就进行扩容,否则计算元素下标位置,如果不存在就新创建一个节点存入。

③ 如果首节点和待插入元素的 hash值和 key 值都一样,直接更新 value 值。

④ 如果首节点是 TreeNode 类型,调用 putTreeVal 方法增加一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,否则往右子树查找,找到空位后执行两个方法:balanceInsert 方法,一方面把节点插入红黑树,一方面对红黑树进行调整使之平衡。moveRootToFront 方法,由于红黑树调整平衡后 root节点可能变化,table里记录的节点不再是根节点,需要重置根节点。

⑤ 如果是链表节点,就遍历链表,根据 hash值和 key 值判断是否重复,决定更新值还是新增节点。如果遍历到了链表末尾,添加链表元素,如果达到了建树阈值,还需要调用 treeifyBin 方法把链表重构为红黑树。

⑥ 存放元素后,将 modCount 值加 1,如果节点数 + 1大于扩容阈值,还需要进行扩容。

2 get 方法:根据 key 获取元素的 value 值

① 调用 getNode 方法获取 Node 节点,如果不是 null 值就返回 Node 节点的 value 值,否则返回 null。

② 如果数组不为空,先比较第一个节点和要查找元素的 hash 值和 key 值,如果都相同则直接返回。

③ 如果第二个节点是 TreeNode 节点则调用 getTreeNode 方法进行查找,否则遍历链表根据 hash 值和 key 值进行查找,如果没有找到就返回 null。

3 hash 方法:计算元素 key 对应的散列值

Java 8 的计算过程简单了许多,如果 key 非空就将 key 的 hashCode() 返回值的高低16位进行异或操作,这主要是为了让尽可能多的位参与运算,让结果中的 0 和 1 分布得更加均匀,从而降低哈希冲突的概率。

4 resize 方法:扩容数组

重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列。

重新规划长度

① 如果 size 超出扩容阈值,把 table 容量增加为之前的2倍。

② 如果新的 table 容量小于默认的初始化容量16,那么将 table 容量重置为16。

③ 如果新的 table 容量大于等于最大容量,那么将阈值设为 Integer 的最大值,并且 return 终止扩容,由于 size 不可能超过该值因此之后不会再发生扩容。

重新排列数据节点

① 如果节点为 null 值则不进行处理。

② 如果节点不为 null 值且没有next节点,那么重新计算其散列值然后存入新的 table 数组中。

③ 如果节点为 TreeNode 节点,那么调用 split 方法进行处理,该方法用于对红黑树调整,如果太小会退化回链表。

④ 如果节点是链表节点,需要将链表拆分为 hashCode() 返回值超出旧容量的链表和未超出容量的链表。对于hash & oldCap == 0的部分不需要做处理,反之需要放到新的下标位置上,新下标 = 旧下标 + 旧容量。


补充:红黑树

红黑树是一种自平衡的二叉查找树。

特性: 红黑树的每个节点只能是红色或者黑色、根节点是黑色的、每个叶子节点都是黑色的、如果一个叶子节点是红色的,它的子结点必须是黑色的、从一个节点到该节点的叶子节点的所有路径都包含相同数目的黑色节点。

左旋: 对 a 节点进行左旋,指将 a 节点的右节点作为 a 的父节点,即将a变成一个左节点。

右旋: 对 a 节点进行右旋,指将 a 节点的左节点作为 a 的父节点,即将a变成一个右节点。

添加

红黑树的添加分为3步:① 将红黑树看作一颗二叉查找树,并以二叉树的插入规则插入新节点。② 将插入的节点设为红色或黑色。③ 通过左旋、右旋或变色,使之重新成为一棵红黑树。

根据被插入节点的父节点情况,可以将插入分为3种情况处理:

  • 被插入的节点是根节点,直接将其涂为黑色。
  • 被插入节点的父节点是黑色的,不做处理,节点插入后仍是红黑树。
  • 被插入节点的父节点是红色的,一定存在非空祖父节点,进一步分为三种情况处理:
    • 叔叔节点是红色的,将父节点设为黑色,叔叔节点设为黑色,祖父节点设为红色,将祖父节点作为当前节点。
    • 叔叔节点是黑色的且当前节点是右节点,则将父节点设为当前节点,以新节点为支点左旋。
    • 叔叔节点是黑色的且当前节点是左节点,则将父节点设为黑色,祖父节点设为红色,以祖父节点为支点右旋。

删除

红黑树的添加分为3步:① 将红黑树看作一颗二叉查找树,并以二叉树的删除规则删除新节点。② 通过左旋、右旋或变色,使之重新成为一棵红黑树。

根据被删除节点的情况,可以将删除分为3种情况处理:

  • 被删除的节点没有子节点,直接将其删除。
  • 被删除节点只有一个子节点,直接删除该节点,并用其唯一子节点替换其位置。
  • 被插入节点有两个子节点,先找出该节点的替换节点,然后把替换节点的数值复制给该节点,删除替换节点。

通过左旋、右旋或变色使其重新成为红黑树。如果当前节点的子节点是一红一黑,直接将该节点设为黑色。如果当前节点的子结点都是黑色,且当前节点是根节点,则不做处理。如果当前节点的子节点都是黑色且当前节点不是根节点,分为以下几种情况:

  • 当前节点的兄弟节点是红色的,就将当前节点的兄弟节点设为黑色,将父节点设为红色,对父节点左旋,重新设置当前节点的兄弟节点。
  • 当前节点的兄弟节点是黑色的,兄弟节点的两个子节点也都是黑色的,则将当前节点的兄弟节点设为红色,将当前节点的父节点作为新节点。
  • 当前节点的兄弟节点是黑色的,兄弟节点的左节点是红色右节点是黑色,将当前节点的左子结点设为黑色,将兄弟节点设为红色,对兄弟节点右旋、重新设置兄弟节点。
  • 当前节点的兄弟节点是黑色的,兄弟节点的右节点是红色的,将当前节点的父节点赋给兄弟节点,将父节点设为黑色,将兄弟节点的右子节点设为黑色,对父节点左旋,设置当前节点为根节点。

猜你喜欢

转载自blog.csdn.net/qq_41112238/article/details/106716742