只针对 JDK1.8 版本之后的 HashMap
HashMap 的底层存储结构为数组+链表/红黑树。数组作为 HashMap 的主体,以 Node<K, V>[]
的形式存储,每个节点存储 <K, V> 键值对,支持以 null 值作为 key 或者 value,但也要保证 key 的唯一性,即以 null 作为 key 只能有一个。
HashMap 的默认数组初始化大小为16,扩容时变为原来的 2 倍。并且 HashMap 总是以 2 的幂作为 数组的大小。
hash 值的计算
对于每个 key 来说,都有自己的 hashCode(),以此来计算 HashMap 中的 hash 值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后通过 (n - 1) & hash
判断当前元素存放的位置。
冲突处理
如果不同元素的位置发生冲突,则采用链地址法,在该位置添加链表,使用尾插法插入,即将新元素添加至链表的尾部。
为什么不使用头插法
在多线程的环境下,使用头插法可能导致链表成环,故牺牲时间采用尾插法,尾插法也会有节点丢失的问题,故多线程情况下应该使用 CurrentHashMap。
若此时链表长度超过了阈值(默认为 8),并且数组的长度大于等于 64,则会将数组转化为红黑树,以提高查询效率。( 链表查找时间复杂度为O(n),红黑树查找的时间复杂度为O(log(n) )
数组扩容
当 HashMap 中的元素越来越多时,由于数组的长度是固定的,Hash 碰撞的几率会越来越高,影响查询效率。
数组扩容时,会遍历所有的元素,根据其 hash 值进行重新的分配,是非常耗时的。
数组扩容每次将数组的大小扩大一倍: newCap = oldCap << 1
数组为什么每次扩容为原大小的二倍?
-
容量为2的幂可以使元素均匀的分布在数组中,可以减少 Hash 碰撞。
-
数组扩容后,不需要重新计算每个元素的新位置,根据
(n - 1) & hash
,n 扩大一倍,相当于 (n - 1) 的高位多了1,此时再看对应 hash 值的同一位,若该位值为 0,则元素下标不变,若值为 1,(n - 1) & hash
多了一位,相当于元素的位置增加了原长度的大小,即元素新下标 = 元素原下标+原数组长度举个简单的例子:
数组长度为 8:
-
(8 - 1) & 3 = 00111 & 00011 = 00011 = 3;
-
(8 - 1) & 15 = 00111 & 11111 = 00111 = 7;
数组扩容后长度为 16:
-
(16 - 1) & 3 = 01111 & 001011 = 00011 = 3;
-
(16 - 1) & 15 = 11111 & 11111 = 11111 = 15;
-
触发机制
-
加载因子默认大小为 0.75f,如果超过该阈值:
size == (capacity * load factor)
,则会触发 resize() 进行数组的扩容操作。 -
如果某一链表长度超过了阈值(默认为 8),而此时的数组长度是小于 64 的,也会触发数组的扩容操作。