没有比这个更简洁的HashMap 源码分析解读

HashMap 作为最常用的集合类之一,有必要深入浅出的了解一下。这篇文章会深入到 HashMap 源码,刨析它的存储结构以及工作机制。

1. HashMap 的存储结构

HashMap 的数据存储结构是一个 Node<K,V> 数组,在(Java 7 中是 Entry<K,V> 数组,但结构相同)

没有比这个更简洁的HashMap 源码分析解读

存储结构主要是数组加链表,像下面的图。

没有比这个更简洁的HashMap 源码分析解读

HashMap 存储结构(图片来自网络)

2. HashMap 的 put()

在 Java 8 中 HashMap 的 put 方法如下,我已经详细注释了重要代码。

没有比这个更简洁的HashMap 源码分析解读

举个例子,如果 put 的 key 为字母 a,当前 HashMap 容量是初始容量 16,计算出位置是 1。

没有比这个更简洁的HashMap 源码分析解读

总结 HashMap put 过程。

  1. 计算 key 的 hash 值。计算方式是 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  2. 检查当前数组是否为空,为空需要进行初始化,初始化容量是 16 ,负载因子默认 0.75
  3. 计算 key 在数组中的坐标。计算方式:(容量 - 1) & hash.因为容量总是2的次方,所以-1的值的二进制总是全1。方便与 hash 值进行运算。
  4. 如果计算出的坐标元素为空,创建节点加入,put 结束。
    1. 如果当前数组容量大于负载因子设置的容量,进行扩容
  5. 如果计算出的坐标元素有值。
    1. 如果 next 节点为空,把要加入的值和 key 加入 next 节点。
    2. 如果 next 节点不为空,循环查看 next 节点。如果发现有 next 节点的 key 和要加入的 key 一样,对应的值替换为新值。
    3. 如果循环 next 节点查找超过8层还不为空,把这个位置元素转换为红黑树
    4. 如果坐标上的元素值和要加入的值 key 完全一样,覆盖原有值。
    5. 如果坐标上的元素是红黑树,把要加入的值和 key 加入到红黑树。
    6. 如果坐标上的元素和要加入的元素不同(尾插法增加)。

3. HashMap 的 get()

在 Java 8 中 get 方法源码如下,我已经做了注释说明。

没有比这个更简洁的HashMap 源码分析解读

get 方法流程总结。

  1. 计算 key 的 hash 值。
  2. 如果存储数组不为空,且计算得到的位置上的元素不为空。继续,否则,返回 Null。
  3. 如果获取到的元素的 key 值相等,说明查找到了,返回元素。
  4. 如果获取到的元素的 key 值不相等,查找 next 节点的元素。
    1. 如果元素是红黑树,在红黑树中查找。
    2. 不是红黑树,遍历 next 节点查找,找到则返回。

4. HashMap 的 Hash 规则

  1. 计算 hash 值 int hash = key.hashCode()。
  2. 与或上 hash 值无符号右移16 位。hash = hash ^ (hash >>> 16)。
  3. 位置计算公式 index = (n - 1) & hash ,其中 n 是容量。

5. HashMap 的初始化大小

  1. 初始化大小是 16,为什么是 16 呢?这可能是因为每次扩容都是 2 倍。而选择 2 的次方值 16 作为初始容量,有利于扩容时重新 Hash 计算位置。为什么是 16 我想是一个经验值,理论上说只要是 2 的次方都没有问题。

6. HashMap 的扩容方式

负载因子是多少?负载因子是 0.75

扩容方式是什么?看源码说明。

d92c8dc5-3e98-4fed-a603-c05650bf889duploading.4e448015.gif转存失败重新上传取消没有比这个更简洁的HashMap 源码分析解读

扩容时候怎么重新确定元素在数组中的位置,我们看到是由 if ((e.hash & oldCap) == 0) 确定的。

没有比这个更简洁的HashMap 源码分析解读

通过上面的分析也可以看出,只有在每次容量都是2的次方的情况下才能使用 if ((e.hash & oldCap) == 0) 判断扩容后的位置。

7. HashMap 中的红黑树

HashMap 在 Java 8 中的实现增加了红黑树,当链表节点达到 8 个的时候,会把链表转换成红黑树,低于 6 个的时候,会退回链表。究其原因是因为当节点过多时,使用红黑树可以更高效的查找到节点。毕竟红黑树是一种二叉查找树。

  1. 节点个数是多少的时候,链表会转变成红黑树。链表节点个数大于等于 8 时,链表会转换成树结构。
  2. 节点个数是多少的时候,红黑树会退回链表。节点个数小于等于 6 时,树会转变成链表。
  3. 为什么转变条件 8 和 6 有一个差值。如果没有差值,都是 8 ,那么如果频繁的插入删除元素,链表个数又刚好在 8 徘徊,那么就会频繁的发生链表转树,树转链表。

8. 为啥容量都是2的幂?

容量是2的幂时,key 的 hash 值然后 & (容量-1) 确定位置时碰撞概率会比较低,因为容量为 2 的幂时,减 1 之后的二进制数为全1,这样与运算的结果就等于 hash值后面与 1 进行与运算的几位。

下面是个例子。

没有比这个更简洁的HashMap 源码分析解读

如果是其他的容量值,假设是9,进行与运算结果碰撞的概率就比较大。

没有比这个更简洁的HashMap 源码分析解读

另外,每次都是 2 的幂也可以让 HashMap 扩容时可以方便的重新计算位置

没有比这个更简洁的HashMap 源码分析解读

9. 快速失败(fail—fast)

HashMap 遍历使用的是一种快速失败机制,它是 Java 非安全集合中的一种普遍机制,这种机制可以让集合在遍历时,如果有线程对集合进行了修改、删除、增加操作,会触发并发修改异常。

它的实现机制是在遍历前保存一份 modCount ,在每次获取下一个要遍历的元素时会对比当前的 modCount 和保存的 modCount 是否相等。

快速失败也可以看作是一种安全机制,这样在多线程操作不安全的集合时,由于快速失败的机制,会抛出异常。

10. 线程安全的 Map

  1. 使用 Collections.synchronizedMap(Map) 创建线程安全的 Map.实现原理:有一个变量 final Object mutex; ,操作方法都加了这个 synchronized (mutex) 排它锁。
  2. 使用 Hashtable
  3. 使用 ConcurrentHashMap

<完>

发布了255 篇原创文章 · 获赞 53 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Sqdmn/article/details/105225651