HashMap数组的长度为什么是 2 的幂次方?
2 的 N 次幂有助于减少碰撞的几率。如果 length 为2的幂次方,则 length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。我们来举个例子,看下图
当 length =15时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,4和5的结果也是一样,这样就会导致查询速度降低。
如果我们进一步分析,还会发现空间浪费非常大,以 length=15 为例,在 1、3、5、7、9、11、13、15 这八处没有存放数据。因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。
再补充数组容量计算的小奥秘。
HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。会取大于或等于这个数的 且最近的2次幂作为 table 数组的初始容量,使用tableSizeFor(int)方法,如 tableSizeFor(10) = 16(2 的 4 次幂),tableSizeFor(20) = 32(2 的 5 次幂),也就是说 table 数组的长度总是 2 的次幂。JDK 8 源码如下:
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再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。
10、HashMap 的put方法流程?
以JDK 8为例,简要流程如下:
1、首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
2、如果数组是空的,则调用 resize 进行初始化;
3、如果没有哈希冲突直接放在对应的数组下标里;
4、如果冲突了,且 key 已经存在,就覆盖掉 value;
5、如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。
11、HashMap 的扩容方式?
HashMap 在容量超过负载因子所定义的容量之后,就会扩容。
12、一般用什么作为HashMap的key?
一般用Integer、String 这种不可变类当作 HashMap 的 key,String 最为常见。
- 因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。
- 因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的。Integer、String 这些类已经很规范的重写了 hashCode() 以及 equals() 方法。
#13、HashMap为什么线程不安全?
- JDK 7 时多线程下扩容会造成死循环。
- 多线程的put可能导致元素的丢失。
- put和get并发时,可能导致get为null。