1.基本介绍
HashMap是处理键值映射的数据结构,它不保证插入顺序,允许插入 null 的键和值。基于散列表实现,使用拉链法处理碰撞。HashMap的桶数组元素通过一个Node结构实现。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //hash值用于计算hash索引
final K key;
V value;
Node<K,V> next; //后继节点,下一个Node
}
HashMap桶数组默认大小为16。如果在初始化时设置的初始容量不是2的次幂,内部实际创建的容量会是一个大于且靠近2次幂的值。
2.散列函数
散列函数用的是除留余数法。未了均匀地散列键的散列值,通常都会把数组的大小取素数。HashMap 的容量始终是 2 的次幂,之所以这样设计,是为了将取模运算转为位运算,提高性能。hash%length=hash&(length-1)成立的原因。
2^1 = 10 2^1 -1 = 01
2^2 = 100 2^2 -1 = 011
2^3 = 1000 2^3 -1 = 0111
2^n = 1(n个零) 2^n -1 = 0(n个1)
可以发现当 length = 2^n 时,h & (length-1) 的结果正好位于 0 到 length-1 之间,就相当于取模运算。
转为位运算后,length-1 就相当于一个低位掩码,在按位与时,它会把散列值hash的高位置0,这就导致散列值只在掩码的小范围内(0 到 length-1 )变化,显然增大了冲突几率。为了减少冲突,HashMap 计算hash时,使用高低位异或,变相的让键的高位也参与了运算,代码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} //h值高16位与低16位异或。
高位的移位异或,既能保证有效的利用键的高低位信息,又能减少系统开销,这样设计是对速度、效率和质量之间的权衡。
3.扩容
HashMap创建时,默认情况下,初始容量是 16,负载因子是 0.75f,threshold 是 12,也就是说,插入 12 个键值对就会扩容。扩容时也会扩大为原来的两倍。扩容为原来两倍的好处是:元素的在新数组的桶位置中要么保持不变,要么在原来的位置上+扩容大小
扩容后length-1只有末位0发生变化置为了1,因此扩容后原来的元素位置会被划分成两种:1.和原数组所在位置相同;2.在原数组的位置基础上的+扩容的长度length。扩容移动位置时只需要通过hash&oldLengt==0判断在新数组的位置,当为0时为位置不变。Java8中resize()方法中的移动操作。
if ((e.hash & oldCap) == 0) {
...
}else {
...
}
4.转红黑树
当某个桶的下的链表长度超过 8且元素个数超过64时,会转换为红黑树,提升查询效率O(N)变为O(LogN)。 提升效率的同时也增加了内存空间。树节点的大小是链表节点的两倍。
为什么是8,JDK8中源码注释说明
Ideally, under random hashCodes, the frequency of nodes in bins follows
a Poisson distribution with a parameter of about 0.5 on average
for the default resizing threshold of 0.75,
although with a large variance because of resizing granularity.
Ignoring variance, the expected occurrences of list size k
are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
理想情况下,在随机哈希码下,哈希表中节点的频率遵循泊松分布,当出现频率为8时的概率已经很小,从8变为6的概率也小,树结构和链表结构不会频繁转化。同时红黑树平均查找长度为log(8)=3,链表平均查找长度为4。这样转化为红黑树才有意义。
5.懒加载
当调用HashMap无参构造函数时,并没有进行哈希表Node[]的初始化。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
而是在put时,发现哈希表为空才进行初始化,调用resize()方法进行初始化。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; 开始初始化
....
}