HashMap底层的数据结构为数组+单链表+红黑树,它最多只允许一个记录的键为null,但可以有多条记录的值为null。
存储结构
数组
1 | transient Node<K,V>[] table; |
单链表
1 | static class Node<K,V> implements Map.Entry<K,V> { |
红黑树
1 | // LinkedHashMap.Entry<K,V>又继承了HashMap.Node<K,V> |
静态常量
threshold = capacity × load factor
1 | // 默认情况下,表的初始化容量 |
构造方法
1 | public HashMap(int initialCapacity, float loadFactor) { |
tableSizeFor方法用于计算满足initialCapacity <= x的最小x。其中,x是2的整数次幂。
1 | static final int tableSizeFor(int cap) { |
也就是说,不管给定的初始容量是多少,最终HashMap的容量都将是2的整数次幂。
put操作
1 | public V put(K key, V value) { |
hash方法的目的在于将给定的key转换成一个整数。如果key为null(最多只允许有一条记录的key为null),那么将其放入第0个桶中。否则,让键哈希码h的低16位与高16位做异或运算,使得HashMap中记录分布更加均匀。
1 | static final int hash(Object key) { |
得到hash值以后,通过除留余数法,即对表的长度n进行取模运算,来确定桶下标。
已知当n为2的整数次幂时,hash % n等价于hash & (n - 1),因此,可以使用位运算来优化取模运算。
这也是为什么HashMap的容量必须为2的整数次幂的原因。
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
扩容操作
扩容后的大小为原来容量的2倍。
设原来的容量为n,则扩容以后新的容量为2n。
假设hash % n = j,当对表中元素再次定址时,有以下两种情况:
1.如果hash < n 或者 hash = n × 2a + j (1 <= a < n/2),即hash & n = 0,那么hash % (2n) = j。
此时,之前位于第j个桶中的元素,扩容以后依然放在第j个桶中。
2.否则,hash % (2n) = n + j。
此时,原来放在第j个桶中的元素,扩容以后将放在第n + j个桶中。
1 | final Node<K,V>[] resize() { |
按键取值
1 | public V get(Object key) { |
remove操作
1 | public V remove(Object key) { |
红黑树与单链表的相互转变
单链表=>红黑树
当执行插入操作时,若桶中的结点个数>=树化阈值(8),并且表的容量>=最小树化容量(64),才会将单链表转变为红黑树。
1 | final void treeifyBin(Node<K,V>[] tab, int hash) { |
如果只有桶中的结点个数>=树化阈值(此时,将调用treeifyBin方法)时,可能是因为表容量过小,导致散列不均匀,过多的结点集中在部分桶中。为了防止这种情况的发生,HashMap将尝试扩大容量。
红黑树=>单链表
1.当执行扩容操作时,若桶中的结点个数<=不树化阈值(6),则将红黑树还原为单链表。
1 | // TreeNode的方法,在resize()方法中被调用 |
2.当执行删除操作时,若桶中结点太少,则将红黑树还原为单链表。
1 | // TreeNode的方法,在remove方法中被调用 |