HashMap 作为最常用的集合类之一,有必要深入浅出的了解一下。这篇文章会深入到 HashMap 源码,刨析它的存储结构以及工作机制。
1. HashMap 的存储结构
HashMap 的数据存储结构是一个 Node<K,V> 数组,在(Java 7 中是 Entry<K,V> 数组,但结构相同)
存储结构主要是数组加链表,像下面的图。
HashMap 存储结构(图片来自网络)
2. HashMap 的 put()
在 Java 8 中 HashMap 的 put 方法如下,我已经详细注释了重要代码。
举个例子,如果 put 的 key 为字母 a,当前 HashMap 容量是初始容量 16,计算出位置是 1。
总结 HashMap put 过程。
- 计算 key 的 hash 值。计算方式是 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- 检查当前数组是否为空,为空需要进行初始化,初始化容量是 16 ,负载因子默认 0.75。
- 计算 key 在数组中的坐标。计算方式:(容量 - 1) & hash.因为容量总是2的次方,所以-1的值的二进制总是全1。方便与 hash 值进行与运算。
- 如果计算出的坐标元素为空,创建节点加入,put 结束。
- 如果当前数组容量大于负载因子设置的容量,进行扩容。
- 如果计算出的坐标元素有值。
- 如果 next 节点为空,把要加入的值和 key 加入 next 节点。
- 如果 next 节点不为空,循环查看 next 节点。如果发现有 next 节点的 key 和要加入的 key 一样,对应的值替换为新值。
- 如果循环 next 节点查找超过8层还不为空,把这个位置元素转换为红黑树。
- 如果坐标上的元素值和要加入的值 key 完全一样,覆盖原有值。
- 如果坐标上的元素是红黑树,把要加入的值和 key 加入到红黑树。
- 如果坐标上的元素和要加入的元素不同(尾插法增加)。
3. HashMap 的 get()
在 Java 8 中 get 方法源码如下,我已经做了注释说明。
get 方法流程总结。
- 计算 key 的 hash 值。
- 如果存储数组不为空,且计算得到的位置上的元素不为空。继续,否则,返回 Null。
- 如果获取到的元素的 key 值相等,说明查找到了,返回元素。
- 如果获取到的元素的 key 值不相等,查找 next 节点的元素。
- 如果元素是红黑树,在红黑树中查找。
- 不是红黑树,遍历 next 节点查找,找到则返回。
4. HashMap 的 Hash 规则
- 计算 hash 值 int hash = key.hashCode()。
- 与或上 hash 值无符号右移16 位。hash = hash ^ (hash >>> 16)。
- 位置计算公式 index = (n - 1) & hash ,其中 n 是容量。
5. HashMap 的初始化大小
- 初始化大小是 16,为什么是 16 呢?这可能是因为每次扩容都是 2 倍。而选择 2 的次方值 16 作为初始容量,有利于扩容时重新 Hash 计算位置。为什么是 16 我想是一个经验值,理论上说只要是 2 的次方都没有问题。
6. HashMap 的扩容方式
负载因子是多少?负载因子是 0.75。
扩容方式是什么?看源码说明。
扩容时候怎么重新确定元素在数组中的位置,我们看到是由 if ((e.hash & oldCap) == 0) 确定的。
通过上面的分析也可以看出,只有在每次容量都是2的次方的情况下才能使用 if ((e.hash & oldCap) == 0) 判断扩容后的位置。
7. HashMap 中的红黑树
HashMap 在 Java 8 中的实现增加了红黑树,当链表节点达到 8 个的时候,会把链表转换成红黑树,低于 6 个的时候,会退回链表。究其原因是因为当节点过多时,使用红黑树可以更高效的查找到节点。毕竟红黑树是一种二叉查找树。
- 节点个数是多少的时候,链表会转变成红黑树。链表节点个数大于等于 8 时,链表会转换成树结构。
- 节点个数是多少的时候,红黑树会退回链表。节点个数小于等于 6 时,树会转变成链表。
- 为什么转变条件 8 和 6 有一个差值。如果没有差值,都是 8 ,那么如果频繁的插入删除元素,链表个数又刚好在 8 徘徊,那么就会频繁的发生链表转树,树转链表。
8. 为啥容量都是2的幂?
容量是2的幂时,key 的 hash 值然后 & (容量-1) 确定位置时碰撞概率会比较低,因为容量为 2 的幂时,减 1 之后的二进制数为全1,这样与运算的结果就等于 hash值后面与 1 进行与运算的几位。
下面是个例子。
如果是其他的容量值,假设是9,进行与运算结果碰撞的概率就比较大。
另外,每次都是 2 的幂也可以让 HashMap 扩容时可以方便的重新计算位置。
9. 快速失败(fail—fast)
HashMap 遍历使用的是一种快速失败机制,它是 Java 非安全集合中的一种普遍机制,这种机制可以让集合在遍历时,如果有线程对集合进行了修改、删除、增加操作,会触发并发修改异常。
它的实现机制是在遍历前保存一份 modCount ,在每次获取下一个要遍历的元素时会对比当前的 modCount 和保存的 modCount 是否相等。
快速失败也可以看作是一种安全机制,这样在多线程操作不安全的集合时,由于快速失败的机制,会抛出异常。
10. 线程安全的 Map
- 使用 Collections.synchronizedMap(Map) 创建线程安全的 Map.实现原理:有一个变量 final Object mutex; ,操作方法都加了这个 synchronized (mutex) 排它锁。
- 使用 Hashtable
- 使用 ConcurrentHashMap
<完>