扰动函数
为什么使用扰动函数
增加随机性,让元素散列均匀,减少碰撞。
源码分析
看下hashMap计算hash的源码:
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到,计算hash时,使用hash与右移16位的hash做了异或运算。16位正好是自己二进制长度的一半,之后与原hash的做异或运算,这样就混合了原hash中的高位和低位,增大了随机性。
然后再用这个二进制数字与map容量减一进行与运算,就得到了这个key应该存放的位置。
初始化容量
先说总则:初始化容量只能是2的n次幂,如果声明不是,则自动转换为大于声明容量的最小的2的n次幂。
先看源码:
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
可以看到在初始化容量时,如果容量不小于0并且没有达到最大容量,则调用tableSizeFor()方法。
tableSizeFor()源码如下:
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
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;
}
|= 运算符相当于“或等”,即两个数某一位有一个是1即为1。所以这一串操作下来,其实是把传入的容量cap,变成了111……,正是大于cap的最小的2的几次幂-1,最后返回n+1,就正好是2的几次幂了。
比如传入的是17,二进制为10001
,依次的执行结果如下:
int n = cap - 1 = 10000
n |= n >>> 1 = 11000
n |= n >>> 2 = 11110
n |= n >>> 4 = 11111
n |= n >>> 8; //不需要
n |= n >>> 16; //不需要
这样就得到了11111,即31,最后返回n + 1即32。
为什么一定要是2的n次幂呢?
这就与上面的扰动函数关联起来了。2的n次幂减一正好是11111……这样的形式,与扰动函数的hash进行与运算,可以使散列更加均匀,减少碰撞。
负载因子
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
这是HashMap默认的负载因子,0.75,当使用容量达到75%时,map就会自动扩容。
通过上面的构造函数可以看出来,这个负载因子我们也可以通过构造函数在创建map的时候传进去。
负载因子越小,就越不容易产生碰撞,map的性能也就越好。所以如果希望用空间换时间,可以把负载因子设置的小一些。
map扩容大小为原来的二倍newCap = oldCap << 1
。
扩容元素拆分
map进行扩容后,原来的元素就要拆分到新的map中。JDK1.7时,需要重新计算hash值,比较费时。而JDK8中进行了优化,不再需要重新计算hash值了。
那么JDK8是如何进行拆分的呢?
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// preserve order
Node<K,V> loHead = null, loTail = null;