总结一下:其实这个bug是当初写这个代码的人不小心留下来的,后来才被发现,出于面子他不承认这是个小bug,也不想在去修改了,因为不影响使用所以在后来的维护人员中就把它保留下来了。其实跟你们说这一点就是为了丰富自己的知识,让自己在写代码中有一定的规范意识。
注意:面试题来了:为什么一定要是2的n次幂?
2的n次方实际就是1后面n个0,2的n次方-1实际就是n个1,这句话就是问题的关键(因为都是1的情况下它与不同的hash值进行按位与运算才能保证每个数组位置被分配到的概率相同,要是不明白这句话可以见下图)
hashmap的底层是这样计算索引值的(n - 1) & hash
说明:按位与运算:相同的二进制数位上都是1的时候结果为1,否则为0。
如果是2的n次幂:
长度为8,那么n-1就是7对应的二进制为0111,假设有两个hash值分别是2和3,那么:
3&(8-1)=3;2&(8-1)=2,则不会产生碰撞
如果不是2的n次幂:
长度为9,那么n-1对应的二进制为1000,假设有两个hash值分别是2和3,那么:
3&(9-1)=0;2&(9-1)=0,则会产生碰撞
总结:如果数组长度不是2的n次幂,计算出来的索引特别容易相同,及其容易发生hash碰撞。导致其余数组空间很大程度上并没有存储数据,还会增大某个位置出现链表和红黑树的概率,降低性能和效率。
其实它俩的功能是一样的都是计算索引位置其结果也是一样,但是为什么底层用的是第二种呢,这是因为后者的效率比较高因为前面的十进制数要转成二进制数,而后者直接操作的就是二进制数。
注意:当不考虑效率的时候采用直接取余的形式是不要求数组的长度必须是2的次方的。
注意:面试题来了:为什么要是0.75?
0.75是根据大量的测试得来的,它是根据数组的利用率和产生hash碰撞的概率得出来的。
假如加载因子是0.5,数组长度为12,那么扩容得条件为0.512=6,也就是说当数组存得数据大于6得时候就会扩容,那么会产生一个为原来2倍的数组,你会发现数组的大部分空间都没有利用到,造成了资源的浪费,在循环查询数组时导致查询效率低。
假如加载因子是0.9,数组长度仍为12,那么扩容的条件欸0.912=10.8,也就是说存的数据大于11时就会扩容,那么就会产生一个问题:由于数组长度为12,到11的时候才会进行扩容那么就会增大hash的碰撞概率。(为什么会增大碰撞概率呢?因为数组的位置数目是一定的,当放入的数据越多但是这时候还没有达到扩容的条件并且数组已经快满的时候,就必然会在某些位置产生碰撞。)
以上要是还没有听懂,那我也没办法了。
现在可能看不出区别,但是当你看完下面的骚操作你就能明白为什么要计算hash值了
** 小总结:当采用直接用hashcode值计算索引值的形式时会出现当hashcode值的高位变化很大,而低位变换很小或者没用变,那么如果直接和数组长度进行&运算会很容易造成计算的结果一样,从而导致hash碰撞**
你会发现当高位改变同时又进行了操作但是得到的索引值还是5,你千万不要以为这也没区别啊,你要是感觉没区别就说明你没有真正的理解,这里给你留个思考,区别到底在哪,在文章的最后我会给出答案(1)。
大总结:为什么要这样操作呢?(如果直接和hashCode做按位与运算,实际上只是使用了数组长度减1的低位按照上面的例子数组是16,那么减1后就是15对应的二进制就是0000 0000 0000 0000 0000 0000 0000 1111,如果当hashCode的高位变化很大,低位变化很小,这样的话高位就没有利用起来就很容易产生hash冲突,那么进行了该操作后高位也就利用起来了,那么就大大降低了冲突)
重头戏来了,前面都是过度
仔细阅读上述的内容不难发现扩容的条件(1)数组实际容量大于数组长度*0.75(2)链表长度大于8但数组长度没有达到64
既然刚才聊到了扩容,那接下来看一下到底是怎么扩容的
(e1,next1代表线程1),(e2,next2代表线程2)
假设上面的都能正常执行完后,也就是说e1e2现在指向的是3这个位置,next1和next2现在指向的是2这个位置
假设线程2突然阻塞,不继续执行,而线程1正常执行,那么得到的结果如下:
这时候e2和next2可不是指向上图中的位置了哦,因为数据已经被线程1移到新的数组内了,
假设线程2现在又不阻塞了,正常运行了,那么它也会走之前数据迁移的代码,走的过程中会出现死循环如下图:
附上jdk1.7数据迁移的代码可以按照代码自行画图理解
jdk1.8对这一现象进行了优化,把原本的头插法改成了尾插法这样虽然解决了死循环的问题但是当多个线程进行扩容是会发生数据覆盖。
重点了解一下jdk1.8对于扩容到底是怎么优化的。主要分两点上面已经讲解了一个点,接下来就谈谈第二点
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 初始化节点,第一次为null,第二次16
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 第二次16
int oldThr = threshold; // 第一次12
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { // 容量大于等于最大容量扩容为最大
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) // 阀值扩容 *2
newThr = oldThr << 1; // double threshold 左移1位(*2)
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;// 第一次初始化默认容量16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 初始容量*阀值
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 获取数组中的元素
oldTab[j] = null; // gc
if (e.next == null) // 若下个节点等于null直接存入数组
newTab[e.hash & (newCap - 1)] = e; // 放入新的数组中 若不减1 超过数组最大容量 高位
else if (e instanceof TreeNode) // 红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order 双向列表
Node<K,V> loHead = null, loTail = null; // 低位:loHead头链 loTail尾链
Node<K,V> hiHead = null, hiTail = null; // 高位
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 原下标位置
if (loTail == null) // 低位尾节点为null
loHead = e; // 元素放在头节点
else
loTail.next = e; // 放在尾节点的下个节点 12->13
loTail = e; // 首次头节点=尾节点
}
else {
if (hiTail == null) // 高位尾节点不存在,首次进来尾节点=首节点
hiHead = e; // 放在尾节点 hiHead=1 hiTail=1
else
hiTail.next = e; // 1->2
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) { // 低位尾节点不为null
loTail.next = null; // 首次头节点=尾节点 尾节点gc回收
newTab[j] = loHead; // 放入数组
}
if (hiTail != null) { // 高位插入在原下标+j的位置
hiTail.next = null; // 第一次进来是 1-value
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
扩容就讲到这,想深入了解的话自行百度
总结:在链表长度很小的时候,遍历速度还是很快的,但是当链表长度不断变长,链表的查询性能就不行了。这时候就需要转成红黑树了。当有n个元素红黑树的查询为O(lgn),链表的查询为O(n)
为什么会是8
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);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
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;
}
举例说明:
假如自己设置容量为10,会先减1然后无符号右移最后进行或运算。为什么会先减1呢?后面会给出答案(2)
得到的结果其实就是把原来的二进制数经过一顿骚操作变成全是1的形式然后返回n+1,其实就是为了得到2的n次幂减少hash碰撞。这个函数的目的就是寻找比该数大于的最小的2次方
请点击
请点击
//获取所有的key
Set<String> keys = hashMap1.keySet();
for (String key : keys) {
System.out.println(key);
}
//获取所有的value
Collection<String> values = hashMap1.values();
for (String value : values) {
System.out.println(value);
}
}
(2)
//使用迭代器
Set<Map.Entry<String,String>> entries = hashMap1.entrySet();//获取数组里的所有entry对象
for (Iterator<Map.Entry<String,String>> it = entries.iterator();it.hasNext();) {
Map.Entry<String,String> entry = it.next();
System.out.println(entry.getKey()+"----"+entry.getValue());
}
(3)
//通过key的方式,不建议使用,进行了两次迭代效率低
Set<String> keys = hashMap1.keySet();
for (String key : keys) {
String value = (String )hashMap1.get(key);
System.out.println(value);
}
(4) jdk1.8新增的方法
hashMap1.forEach((key, Value)->{
System.out.println(key+"----"+Value);
});
其实一个公式就解决了,默认的加载因子是0.75,反推一下得出:(需要存储的元素个数 / 负载因子)+ 1 即可得出。这样做可以尽量减少扩容或保证不会扩容。(为什么会减少呢?读到这里你要是还不知道原因请重头在来一遍)要保证把我们的数据全存储起来后也不会发生扩容。
先暂时就这么多
可以用反推法:假设没有减1且传入的值就是2的次方为8会发生什么呢?会直接进行一顿骚操作,直接把1000=8变成了1111=16造成资源浪费,会有很多空间没有被利用到查询效率也会变低。