HashMap源码解析
分析版本: JDK1.8
在 Java8 之前, HashMap 是链表散列的数据结构,即数组和链表的结合体;从 Java8 开始,引入红黑树的数据结构和扩容的优化。
Node
从 Java8 引入红黑树之后, HashMap 是由数组、链表和红黑树组成,发现源码有些地方与之前不同,那就是 Node
:
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
Node ,也就是以前的 Entry
,内容没变,只是换了一种叫法。
put(K key, V value)
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
HashMap 使用哈希表来存储。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java 中 HashMap 采用了链地址法。链地址法,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。
当我们往 HashMap 中 put 元素的时候,先根据 key 的 hashCode 重新计算 hash 值,根据 hash 值再通过高位运算和取模运算得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾,如果该链表超出 8 个的话,就转换成红黑树。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
hash(Object key)
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
定位位置的方法通过以上三个步骤得到,取 key 的 hashCode 的值,然后进行无符号右移 16 位,再与现有哈希桶数组的大小取模。通过 (n - 1) & hash
来得到该对象的保存位求得这个位置的时候,马上就可以知道对应位置的元素,不用遍历链表,大大优化了查询的效率。
当 length 总是 2 的 n 次方时,(length - 1) & hash 运算等价于对 length 取模,也就是h % length,但是 & 比 % 具有更高的效率。
注意,在 Java8 之前的算法是这样的:
1 | static int hash(int h) { |
Java8 优化了高位运算的算法,通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低 Bit 都参与到 Hash 的计算中,同时不会有太大的开销。
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
- 当 table 数组为 null 或者大小为 0 的时候进行扩容。
- 第二步,计算出该 key 的索引 index 。
- 找到 table 数组中该位置,判断该位置上是否有值:
- 如果有,判断 key 的 hashCode() 和 equals() 是否相等,相等的话覆盖,不相等的话再判断是否是红黑树还是链表。
- 如果没有,判断是不是红黑树:
- 是,就插入到红黑树中;
- 不是话,遍历链表,如果大小大于8,转换成红黑树,插入到红黑树中,不大于8的话就插入到链表,如果在遍历的是否发现hashCode() 和 equals() 相等。
- 插入成功后,判断 table 数组大小是否超过了最大容量,是的话扩容
resize()
1 | public class HashMap<K,V> extends AbstractMap<K,V> |