HashMap

101 阅读4分钟

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;      开始初始化
....
}