关于HashMap的详细分析参考博文:
深入学习Java集合之HashMap的实现原理
HashMap死循环分析
【1】HashMap内部数组何时创建?
绝不是初始化时创建,而是在第一次put方法调用的时候创建。咱们从源码分析。
① put方法入口
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
② putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//注意这里,table为null,就会调用resize进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
第一步,创建一个HashMap,放入一个键值对:
public static void main(String[] args){
HashMap map=new HashMap();
map.put("1","1");
}
如下图所示,此时HashMap变量是默认初始化。
第二步,判断table为null,就会调用resize进行“显示初始化”:
如下图所示,旧的临界值、容量都为0:
默认初始化,容量为16,临界值为12:
创建一个新的数组赋值给"table"变量–transient Node<K,V>[] table;:
此时HashMap中的transient Node<K,V>[] table;才是一个真正意义上的数组,然后将键值对1:1放进去。
【2】 HashMap内部table数组容量为什么是2^n?
第一点,未指定initialCapacity时,默认初始化容量为16,然后以后扩容为2倍扩容,从这个意义上来讲肯定是2^n。
那么指定了initialCapacity呢?比如指定3,这是数组的容量是多少?继续看源码。
源码示例如下:
public static void main(String[] args){
HashMap map=new HashMap(3);
map.put("1","1");
}
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);
}
可以看到这里调用了tableSizeFor(initialCapacity),这个方法如**释说明是使用给定的容量返回一个2^n的数。
/**
* 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;
}
此时数组还未初始化,只是根据容量计算了临界值–4。在第一次put时,将会调用resize进行初始化。
如下所示,根据指定的initialCapacity计算一个threshold 然后在这里进行判断并赋值给newCap:
再根据得到的newCap(4)计算出newThr(3):
给int threshold;和transient Node<K,V>[] table;重新赋值:
【3】仔细分析tableSizeFor方法
可以发现tableSizeFor方法保证了无论指定了什么initialCapacity,都会返回一个2^n的数。当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。
下面我们仔细分析tableSizeFor方法。
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;//⑦
}
① cap-1
这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。
② n |= n >>> 1
无符号按位右移运算符。左操作数按位右移右操作数指定的位数,符号位跟着移动,空出来的高位补0(左边补充的值永远为0,不管其最高位(符号位)的值)。
先进行无符号右移一位(相当于n/2),然后再与n进行|运算(输入2个参数,a、b对应位只要有一个为1,c对应位就为1;反之为0)。
由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。
③ n |= n >>> 2;
注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。
④ n |= n >>> 4;
这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。
以此类推
注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就31个1(int最大正数值),但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。
static final int MAXIMUM_CAPACITY = 1 << 30;
图例如下:
【4】hash()方法分析
源码基于jdk1.8:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
从上面的代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)
为什么要这么干呢? 这个与HashMap中table下标的计算有关。
n = table.length;
index = (n-1) & hash;
因为,table的长度都是2的幂,因此index仅与hash值的低n位有关(此n非table.leng,而是2的幂指数),hash值的高位都被与操作置为0了。
假设table.length=2^4=16和某散列值做“与”操作如下,结果就是截取了最低的四位值。
10100101 11000100 00100101
& 00000000 00000000 00001111
----------------------------------
00000000 00000000 00000101 //高位全部归零,只保留末四位
但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比烦人。
时候“扰动函数”的价值就体现出来了,看下面这图:
右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
参考博文:HashMap源码注解 之 静态工具方法hash()、tableSizeFor()