概述
HashMap 和 Hashtable 同作为常见的 Map 实现,两者的区别在于 HashMap 支持 key=null、value=null 而 Hashtable 不支持;HashMap 不支持同步而 Hashtable 采用 synchronized 修饰方法。除此之外,两者提供的功能几乎相同。两者都采取 array+list 的方式存储键值对 Entry,array[i]事实上存储了一个 list 的头结点。但是,相对于 Hashtable,HashMap 会在 array.length(称为 capacity)和 list.size()都达到一定程度之后,将 list 转换成红黑树;而红黑树的结点低于一定数量的时候,也会重新转换成 list。
本文着重 HashMap 与 Hashtable,并不讨论红黑树结构。如有需要,请移步红黑树相关文章。
为了方便叙述,本文中 table 指存储 Entry 的数组结构(Entry[] table);bin 指 table[i]及其 list 或红黑树;capacity 指 table.length,也就是 bin 的数量;threshold 指达到再散列的门槛 size(Entry 总数)。
导航
构造方法
HashMap 和 Hashtable 一样,拥有 loadFactor 字段。如果当前 size>threshold,就会进行再散列以降低散列冲突。如果不指定 loadFactor,两者所有的构造方法都会将 loadFacter 设为默认值(0.75f)。不同之处在于,HashMap 中,table 在需要添加元素的时候初始化,Hashtable 则在构造方法时就初始化了。
1 | // HashMap |
HashMap 和 Hashtable 也都支持从其他 Map 构造(使用了批量插入方法):
散列方法
注意到 HashMap 设置 threshold 时,调用了 tableSizeFor:
1 | // 返回不小于cap的最小2次幂 |
与之相比,Hashtable 的 threshold 设置则要简单许多:
1 | threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); |
为什么 HashMap 要确保 capacity 是 2 的幂次呢?有这么一个公式:
1 | a % (2^n) = a & (2^n - 1) |
一般情况下,我们计算 hash%table.length 来确定存储的 table 下标,就像 Hashtable 一样:
1 | int index = (hash & 0x7FFFFFFF) % tab.length; |
而当 table.length=2 的幂次的时候,我们就可以把 % 运算转换成 & 运算,于是就变成了
1 | int index = (hash & 0x7FFFFFFF) & (tab.length - 1) |
而这,就是 HashMap 的散列方法:
1 | if ((p = tab[i = (n - 1) & hash]) == null) |
所以 HashMap 利用 tableSizeFor 方法,保证容量符合 2^n 形式,从而使散列方法从 % 变成了 -与& 运算,效率提升。
put/get/remove
put
HashMap 除了支持空 key 和空 value 之外,在 bin 使用 list 实现时,put 会在 list 的尾部添加元素,而 Hashtable 则在 list 的头部添加。
1 | // HashMap |
get
与 hashtable 相比,由于 HashMap 支持 null,所以判断 key 是否相等时,使用(k = first.key) == key || (key != null && key.equals(k)))
1 | // HashMap |
remove
remove 的实现过程与 put/get 相似:
1 | // HashMap |
resize
Hashtable 的再散列方法详见 Hashtable 篇。相对于 Hashtable,HashMap 因为对散列方法进行了优化(见上文),再散列会有这些特性:
- newCap = oldCap << 1
- newThr = oldThr << 1
根据以上事实,可以有如下推导(二进制表示更易理解,这里为了方便采用 2^n 表示):
假设 oldCap=2^n,
当 hash&2^n=0 时,hash<2^n,所以 hash&(2^(n+1)-1)=hash&(2^n-1),即 newIndex=oldIndex
当 hash&2^n!=0 时,hash>=2^n,所以 hash&(2^(n+1)-1)=hash&(2^n-1)+2^n,即 newIndex=oldIndex+oldCap
这是 resize 的实现:
1 | final Node<K,V>[] resize() { |