说在前边
JDK1.8中的HashMap较于前代有了较大的变更,主要变化在于扩容机制的改变。在JDK1.7及之前HashMap在扩容进行数组拷贝的时候采用的是头插法,因此会造成并发情景下形成环状链表造成死循环的问题。JDK1.8中改用了尾插法进行数组拷贝,修复了这个问题。同时JDK1.8开始HashMap改用数组+链表/红黑树组合的数据结构来提高查询效率,降低哈希冲突产生的链表过长导致的查询效率减缓现象。
本文的主要内容是对JDK1.8中的扩容机制与前代进行比较,分析其异同。
准备
数组大小的计算
在JDK1.8中,HashMap初始化的大小始终是2的n次幂。为了做到这一点,JDK1.8提供了这样一个方法:
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
// >>> 代表无符号位右移,高位补0
// |= 代表自己和等号后的值按位或并将结果赋给自己
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
在经过这样一系列的运算之后能够保证初始化的数组大小始终大于等于我们自行输入的大小并且是一个离它最近的2的n次幂的数(可自行验算)。
hash值的计算
为了尽量减少数据存放时发生哈希冲突的概率,HashMap会根据每个元素的key计算出一个hash值,用于散列时求出其在数组中的索引位置。具体计算方法如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这一段神秘代码的精髓在于h = key.hashCode() ^ (h >>> 16)这句话。它的作用是让key的hashCode高十六位与第十六位进行异或操作得出较为分散的hash值。这样操作的原因我们放在下面解释。
数组索引的计算
如我们所熟知的HashMap采用数组+链表/红黑树的数据结构进行数据的存储,这就要求我们在存入数据的时候通过一定的运算方式将数据尽可能分散地放入数组中,尽量减少哈希冲突。在历代源码中这个计算方式都是用数组的长度减去一然后和当前元素的key的hash值进行与运算得出。采用这样的方式可以保证散列得到的索引始终落在数组索引范围内不至于索引越界。
在JDK1.6中提供了一个indexFor()方法进行计算,而到了JDK1.8虽然移除了这个方法,计算方式却是一样的。下面是1.6中的方法:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// 这个h就是前文提到的key的hashCode的高十六位与低十六位异或得到的hash值,用它和数组的长度减一进行与运算求出始终落在数组索引范围内的索引值(可以自行举几个例子验算一下)
return h & (length-1);
}
JDK1.8中在放入新数据的时候也是这样的计算方式
if ((p = tab[i = (n - 1) & hash]) == null) // 索引位置为null,放入新数据
tab[i] = newNode(hash, key, value, null);
神秘代码的原理
这时我们就可以考虑为什么要用那