(源码基于JDK1.7)
HashMap的无参实例构造方法默认初始化容量是16(2的4次方)。如果我们使用指定初始化容量的实例构造函数,可以传入一个指定的值,但是实际HashMap在初始化底层存储数据的数组时,会使用一个大于等于指定值的2的幂的数作为数组的初始化容量。并且如果HashMap需要扩容,扩容后容量是原来的2倍,也仍然是一个2的幂,那么HashMap为什么这样来做呢?
我们来从put(K key, V value)方法来一探究竟。先贴上put方法的源码:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
其实原因就在 int i = indexFor(hash, table.length)这个indexFor方法上,贴上该方法源码:
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
我们来看下h&(length-1),h即为根据key获取到的哈希值(其实还做了一些异或和右移的操作,为了在与数组长度进行按位与运算时使高位也能参与到运算中来,提高散列性,对本主题没有影响,可不考虑),length即为数组的容量,如果length的值是一个2的幂,比如16,那么length-1得15,15的二进制表示就是0000 1111(省略了高位,HashMap中数组下标索引是一个int类型,长度为32位,省略高位不影响我们讨论的主题),那么无论哈希值h是多少,与length-1做按位与运算后的结构就是0000至1111,也就是0-15,正好就是数组的下标。举个例子
h: 0000 0001
length-1: 0000 1111
按位与运算的规则是相同位上两个数同时为1,结果才是1,那么实际结果的取值其实就取决于h这个哈希值。这个结果跟用哈希值取模length-1的效果是一样的,但是按位与运算效率较高,所以选择了按位与运算。
说到这里,小伙伴可能会想,那如果数组容量不是2的幂,就不能进行按位与运算吗?答案是当然可以,但是假如数组容量不是2的幂,比如10,换算成二进制表示就是0000 1010,用这个二进制数再与哈希值做按位与运算会出现无论哈希值是多少,得到值的范围就只有
0000(对应十进制的0)、
0010(对应十进制的2)、
1000(对应十进制的8)、
1010(对应十进制的10)这四个,
数组其余6个下标比如0001(对应十进制的1)、0100(对应十进制的4)等,永远无法获取到,这些位置的存储空间也就被浪费了。
总结一下,HashMap为了提高效率而选择了按位与运算而不是取模运算,而只有2的幂的数(实际计算时需减一,因为数组下标是从0开始的)来与哈希值进行按位与运算才能保证某些数组下标不至于因无法计算得到而造成空间的浪费。
Java并发编程的艺术书中是这样描述为什么ConcurrentHashMap下Segment数组容量需要是2的幂的:为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方(power-of-two size),所以必须计算出一个大于或等于concurrencyLevle的最小的2的N次方值来作为segments数组的长度。
但其实实际容量不是2的N次方也不影响使用按位与运算来定位数组的索引,只是说会造成一定空间的浪费。所以大家在学习的过程中,还是尽量都能实际验证下,也是真正掌握知识的一种好的方法。