文章目录
JDK1.8中对HashMap的优化
由 数组+链表 的结构改为 数组+链表+红黑树。
在链表元素数量超过8时改为红黑树,少于6时改为链表,中间7不改是避免频繁转换降低性能。
扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。
不需要重新计算hash,只需要根据原来hash值和容量进行与,0的话索引没变,1的话索引变为原索引加原来的数组长度。
用的尾插法所以新数组链表不会倒置,多线程下不会出现死循环。
数组的长度为什么必须为2^n
原因1:h & (lenght-1)等效 h%lenght 操作,等效的前提就是:length 必须是2的整数倍。
原因2:防止hash冲突,位置冲突。
扩容后下标变化有两种情况:
- 下标不变:将原表下标的元素放到扩容表同样的位置。
- 下标变化:将原表下标的元素加上增加的扩容量放到扩容表的位置。
/**
* 若(e.hash & oldCap) == 0,下标不变,将原表某个下标的元素放到扩容表同样
* 下标的位置上
*/
/**
* 若(e.hash & oldCap) != 0,将原表某个下标的元素放到扩容表中
* [下标+增加的扩容量]的位置上
*/
红黑树
1、根节点为黑色
2、节点不是黑色,就是红色(非黑即红)
HashMap的数据结构
JDK7 中的 HashMap
数组+链表。
HashMap 底层维护一个数据,数组中存放的是 Entry<K, V>。
Map中的key,value则以Entry的形式存放在数组中。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
链表的数据结构是怎样的:
HashMap中,每个元素实际都是一个Node(定义在HashMap内)对象。
从Node的定义上也可以看到,其实是个单向链表。
static class Node<K,V> implements Map.Entry<K,V> {
// 缓存key的hash值
final int hash;
final K key;
V value;
// 下一个Node引用
Node<K,V> next;
// 唯一的一个构造函数
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// equals 等方法
...
}
通过计算 key 的 hash 值来决定放在数组的哪个位置,当 hash 值冲突时,用链表的方式来存储。
数组扩容是原来的一倍。
如果 key 为 null,会将这个元素存放到 table[0] 的位置。
JDK8 中的 HashMap
为了提⾼链表查询性能增加红⿊树,红黑树查询数据的性能大于链表。
1、当链表的长度大于8时。
- 当前hash table的长度小于等于64,会扩容。
- 当前hash table的长度大于64,会转化为一个红黑树。
HashMap初始化
参数解析:
- initialCapacity:初始化容量,默认数组长度为16。
- loadFactor:扩容负载因⼦,默认0.75f,0.75扩容性能最好,没人会改,取的是0.5-1的中间值。
- threshold:扩容阈值,threshold = capacity * loadFactor,当map所容纳的数量达到threshold的值时,hashMap就会自动扩容。
总结就是:当table元素有值数量达到75%时,会进行扩容,扩容2倍。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
HashMap<Integer, Integer> map = new HashMap<>();
Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));
Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));
Field threshold = mapType.getDeclaredField("threshold");
threshold.setAccessible(true);
System.out.println("threshold : " + threshold.get(map));
Field loadFactor = mapType.getDeclaredField("loadFactor");
loadFactor.setAccessible(true);
System.out.println("loadFactor : " + loadFactor.get(map));
HashMap在put时扩容产生死循环
JDK1.7链表使用的是头插法。
JDK1.8链表使用的是尾插法。大大降低了减小发生死循环的概率。
HashMap只有在并发扩容操作的情况下会造成链表的死循环。
HashMap本来就不支持多线程使用,要并发就用ConcurrentHashMap。
在JDK1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全。
HashMap 在并发场景下存在的问题
数据丢失、数据重复、死循环。
数据丢失情况
出现两个线程同时判断 table[i]=null 时,此时两个线程都会去创建Entry,这样存入会出现数据丢失。
数据重复情况
如果有两个线程同时发现自己都 key不存在,而这两个线程的 key 实际是相同的,在向链表中写入的时候,第一个线程将e设置为了自己的Entry,而第二个线程就会执行到e.next,此时拿到的是最后一个节点,依然会将自己持有的数据插入到链表中,这样就出现了数据 重复。
死循环
主要是因为hashMap在 resize 扩容过程中对链表进行了一次倒序处理。
假设两个线程同时进行resize, A->B 第一个线程在处理过程中比较慢,第二个线程已经完成了倒序编程了B-A 那么就出现了循环,B->A->B。就会出现CPU使用率飙升。
之所以出现死循环,主要还是在于对于链表对倒序处理,在Java 8中,已经不在使用倒序列表,解决了死循环问题。
JDK1.8中对hash算法和寻址算法是如何优化的
hash算法优化:用高16异或低16位,使新的数字具备高低16位的特性,这样更加随机不容易冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashCode 值是一个 int 类型(4个字节,32位)。
h >>> 16
,对32位二进制向右位移16位。
(h = key.hashCode()) ^ (h >>> 16)
,对位移前和位移后的二进制进行异或(异或口诀:相同取0,不同取1)。
相当于对高低16位都参与运算。具备高低16位的特征。避免了很多低16位相同hashcode冲突的概率。
有一个key的hash值:
1111 1111 1111 1111 1111 1010 0111 1100
0000 0000 0000 0000 1111 1111 1111 1111 向右位移16位
1111 1111 1111 1111 0000 0101 1000 0011 进行异或
寻址算法优化:用与运算替代取模,提升性能
(n - 1) & hash
=> 与运算效果是跟hash对n取模,效果是一样的,但是与运算的性能要比hash对n取模要高很多。数学问题,如果数组长度是2的n次方,与运算和取模运算的值是一样的,hashmap数组的长度会一直是2的n次方。
与:两个同时为1,结果为1,否则为0
//通过如下代码寻址。其中n为hash table的长度(不是HashMap的size)
i = (n - 1) & hash // 得到数组里的一个位置
1111 1111 1111 1111 0000 0101 1000 0011(经过扰动函数计算的新hash值)
0000 0000 0000 0000 0000 0000 0000 1111 数组的长度,默认是15(从0开始)
0000 0000 0000 0000 0000 0000 0000 0011 进行与运算后的值,转成十进制是3,3表示数组的位置
ConcurrentHashMap基本原理
1、内部持有一个Node<K,V>[]
,用来存放key,value。
这个数组的默认长度是16,并且只会在第一次put的时候才会初始化(lazy init)。
2、put 的时候要通过运算得到应存放的数组下标,然后根据不同的情况决定初始化数组、插入链表、插入红黑树或者协助扩容。
- 先进行hash扰动。
- 数组如果还未进行初始化,则先进行初始化。初始化默认大小为16,如果指定了初始化大小,则会计算一个>=指定值,且为2的N次幂的数字,且最接近当前参数的数字作为初始长度。
- 当前位置==null,则直接通过CAS插入数据。
- 如果当前数组正在进行扩容,则协助扩容。
- 当前位置!=null。如果当前节点是红黑树,则直接插入树中。否则作为链表插入链表插入或者更新。
- 插入成功后,如果是链表,则检查是否需要转成红黑树。转换条件是链表节点数>=8,且数组长度>64。
- 最后更新size的值,并且检查是否需要扩容。
3、get的时候同样通过运算得到应存放的数组下标,然后进行遍历。
- 先进行hash扰动,使用hash&(n-1)得到数组索引。
- 取索引对应的数据进行遍历。可能是链表、红黑树,也可能是FWD节点。
put()
1、hash扰动
2、死循环put,直到成功
- 数组未初始化,则进行初始化
- 元素为空,则进行CAS插入
- 元素正在转移,则协助转移
- 存在hash冲突,则锁住头节点,进行插入
- 超过阈值,链表转红黑树
3、size+1&检查扩容
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
hash扰乱
int hash = spread(key.hashCode());
int binCount = 0;
死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
数组未初始化,则进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
所属节点不为空,则CAS插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
存在hash冲突,则锁住头节点
synchronized (f) {
if (tabAt(tab, i) == f) {
链表
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
超过阈值,链表转红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
size+1&检查是否需要扩容
addCount(1L, binCount);
return null;
}
ConcurrentHashMap 线程安全原理
JDK7
使用分段锁,分割成一个个Segment数组,都继承了ReentrantLock类,也就是说每个Segment类本身就是一个锁。
JDK8
使用了CAS+Synchronized。
插入数据时,当前table位置为空时,则直接通过CAS插入数据。
插入数据时,当前table位置不为空时,使用Synchronized锁住当前元素,再插入数据。
ConcurrentHashMap 1.8为什么要使用CAS+Synchronized取代Segment+ReentrantLock
1.8以前的ConcurrentHashMap是怎么保证线程并发的,首先在初始化ConcurrentHashMap的时候,会初始化一个Segment数组,容量为16,而每个Segment呢,都继承了ReentrantLock类,也就是说每个Segment类本身就是一个锁,之后Segment内部又有一个table数组,而每个table数组里的索引数据呢,又对应着一个Node链表。
当我们使用put方法的时候,是对我们的key进行hash拿到一个整型,然后将整型对16取模,拿到对应的Segment,之后调用Segment的put方法,然后上锁,请注意,这里lock()的时候其实是this.lock(),也就是说,每个Segment的锁是分开的。
其中一个上锁不会影响另一个,此时也就代表了我可以有十六个线程进来,而ReentrantLock上锁的时候如果只有一个线程进来,是不会有线程挂起的操作的,也就是说只需要在AQS里使用CAS改变一个state的值为1,此时就能对代码进行操作,这样一来,我们等于将并发量/16了。
请注意Synchronized上锁的对象,请记住,Synchronized是靠对象的对象头和此对象对应的monitor来保证上锁的,也就是对象头里的重量级锁标志指向了monitor,而monitor呢,内部则保存了一个当前线程,也就是抢到了锁的线程。
那么这里的这个f是什么呢?它是Node链表里的每一个Node,也就是说,Synchronized是将每一个Node对象作为了一个锁,这样做的好处是什么呢?将锁细化了,也就是说,除非两个线程同时操作一个Node,注意,是一个Node而不是一个Node链表哦,那么才会争抢同一把锁.
如果使用ReentrantLock其实也可以将锁细化成这样的,只要让Node类继承ReentrantLock就行了,这样的话调用f.lock()就能做到和Synchronized(f)同样的效果,但为什么不这样做呢?
请大家试想一下,锁已经被细化到这种程度了,那么出现并发争抢的可能性还高吗?还有就是,哪怕出现争抢了,只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销.
但如果是ReentrantLock呢?它则只有在线程没有抢到锁,然后新建Node节点后再尝试一次而已,不会自旋,而是直接被挂起,这样一来,我们就很容易会多出线程上下文开销的代价.当然,你也可以使用tryLock(),但是这样又出现了一个问题,你怎么知道tryLock的时间呢?在时间范围里还好,假如超过了呢?
所以,在锁被细化到如此程度上,使用Synchronized是最好的选择了.这里再补充一句,Synchronized和ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量,Synchronized是唤醒锁池里所有的线程+刚好来访问的线程,而ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程.
如果是线程并发量不大的情况下,那么Synchronized因为自旋锁,偏向锁,轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比ReentrantLock高效。
面试题
HashMap的loadFactor为什么是0.75
问题: 为什么是0.75 , 不是0.5或者1?
回答: 如果是0.5 , 那么每次达到容量的一半就进行扩容,默认容量是16, 达到8就扩容成32,达到16就扩容成64, 最终使用空间和未使用空间的差值会逐渐增加,空间利用率低下。 如果是1,那意味着每次空间使用完毕才扩容,在一定程度上会增加put时候的时间。
继续问: 为什么是0.75,不是0.6或者0.8?
继续回: 取中间值,因为0.75是 0.5 ~ 1的中间值。
还是继续问:
还是继续回: 好像是根据一个数学公式计算得来的,再具体就不清楚了。
JDK1.8中HashMap在出现hash碰撞时链表长度超过8一定会变成红黑树?
不一定。
实际上转换红黑树有个大前提,就是当前hash table的长度也就是HashMap的capacity(不是size)不能小于64,小于64就只是做个扩容。
使用HashMap时,需要注意什么
尽可能避免频繁扩容,数据量大时,初始化时手动指定HashMap大小。
尽可能避免hash碰撞,作为HashMap的key的对象的hashcode方法,要合理设计。
不要在多线程情况下使用,HashMap是同步的,多线程情况下,优先考虑ConcurrentHashMap。
Q1:为什么多线程环境下不能使用HashMap?
A:多线程环境下HashMap可能会导致链表闭环,造成CPU100%
为什么说数据量大时,最好手动指定HashMap大小
空间上的开销:
HashMap的底层是数组+链表/红黑树来实现的,当数组被使用了75%(按默认的负载因子0.75)时就会对数组进行扩容。
而数组的存储空间是连续的,频繁的扩容,导致HashMap需要不停的去申请越来越大的连续的内存空间,当在堆内存中没有足够大的空闲的连续空间时,就会不停的触发GC。
性能上的开销:
依旧是频繁的扩容导致的。
在扩容的时候,除了内存空间之外,每次扩容时,还需要将HashMap中所有元素按照扩容后的hash table大小,重新计算下位置,而红黑树也有可能因为扩容后,重新退化成链表。
这个计算量还是很大的。
ConcurrentHashMap面试题
HaspMap的数组长度为什么是2的N次方?
- 为了避免hash冲突,尽可能的散列数据。
- 提升性能,如果数组长度是2的n次方,与运算和取模运算的值是一样的,与运算性能要高于取模运算。
ConcurrentHaspMap是如何保证线程安全的?
- JDK 1.8以前,多个数组,分段加锁,一个数组一个锁。
- JDK 1.8以后,优化细粒度,一个数组,每个元素进行CAS,如果失败说明有人了,此时synchronized对数组元素加锁,链表+红黑树处理,对数组每个元素加锁。
ConcurrentHaspMap中数据存储的可能形式有哪些?
数组、链表、红黑树。
ConcurrentHaspMap的扩容机制是什么?
多线程通过CAS+synchronized并发扩容。
put 方法
流程:
1)通过hash(Object key)算法得到hash值;
2)判断table是否为null或者长度为0,如果是执行resize()进行扩容;
3)通过hash值以及table数组长度得到插入的数组索引i,判断数组table[i]是否为空或为null;
4)如果table[i] == null,直接新建节点添加,转向 8),如果table[i]不为空,转向 5);
5)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,这里的相同指的是hashCode以及equals,否则转向 6);
6)判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转7);
7)遍历table[i],判断链表长度是否大于8,大于8的话,再判断容量是否大于64,小于64,进行扩容,大于64把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
8)插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
putVal方法
通过putVal方法将传递的key-value对添加到数组table中。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
/**
* 如果当前HashMap的table数组还未定义或者还未初始化其长度,则先通过resize()进行扩容,
* 返回扩容后的数组长度n
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过数组长度与hash值做按位与&运算得到对应数组下标,若该位置没有元素,则new Node直接将新元素插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//否则该位置已经有元素了,我们就需要进行一些其他操作
else {
Node<K,V> e; K k;
//如果插入的key和原来的key相同,则替换一下就完事了
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/**
* 否则key不同的情况下,判断当前Node是否是TreeNode,如果是则执行putTreeVal将新的元素插入
* 到红黑树上。
*/
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不是TreeNode,则进行链表遍历
else {
for (int binCount = 0; ; ++binCount) {
/**
* 在链表最后一个节点之后并没有找到相同的元素,则进行下面的操作,直接new Node插入,
* 但条件判断有可能转化为红黑树
*/
if ((e = p.next) == null) {
//直接new了一个Node
p.next = newNode(hash, key, value, null);
/**
* TREEIFY_THRESHOLD=8,因为binCount从0开始,也即是链表长度超过8(包含)时,
* 转为红黑树。
*/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 转化时会判断数组容量是否大于64,小于64进行扩容,大于64转为红黑树
treeifyBin(tab, hash);
break;
}
/**
* 如果在链表的最后一个节点之前找到key值相同的(和上面的判断不冲突,上面是直接通过数组
* 下标判断key值是否相同),则替换
*/
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;
//onlyIfAbsent为true时:当某个位置已经存在元素时不去覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//最后判断临界值,是否扩容。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize方法
HashMap通过resize()方法进行扩容,容量规则为2的幂次。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//以前的容量大于0,也就是hashMap中已经有元素了,或者new对象的时候设置了初始容量
if (oldCap > 0) {
//如果以前的容量大于限制的最大容量1<<30,则设置临界值为int的最大值2^31-1
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
/**
* 如果以前容量的2倍小于限制的最大容量,同时大于或等于默认的容量16,则设置临界值为以前临界值的2
* 倍,因为threshold = loadFactor*capacity,capacity扩大了2倍,loadFactor不变,
* threshold自然也扩大2倍。
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/**
* 在HashMap构造器Hash(int initialCapacity, float loadFactor)中有一句代码,this.threshold
* = tableSizeFor(initialCapacity), 表示在调用构造器时,默认是将初始容量暂时赋值给了
* threshold临界值,因此此处相当于将上一次的初始容量赋值给了新的容量。什么情况下会执行到这句?当调用
* 了HashMap(int initialCapacity)构造器,还没有添加元素时
*/
else if (oldThr > 0)
newCap = oldThr;
/**
* 调用了默认构造器,初始容量没有设置,因此使用默认容量DEFAULT_INITIAL_CAPACITY(16),临界值
* 就是16*0.75
*/
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//对临界值做判断,确保其不为0,因为在上面第二种情况(oldThr > 0),并没有计算newThr
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
table = newTab;
if (oldTab != null) {
//遍历将原来table中的数据放到扩容后的新表中来
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//没有链表Node节点,直接放到新的table中下标为【e.hash & (newCap - 1)】位置即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是treeNode节点,则树上的节点放到newTab中
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果e后面还有链表节点,则遍历e所在的链表,
else { // 保证顺序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//记录下一个节点
next = e.next;
/**
* newTab的容量是以前旧表容量的两倍,因为数组table下标并不是根据循环逐步递增
* 的,而是通过(table.length-1)& hash计算得到,因此扩容后,存放的位置就
* 可能发生变化,那么到底发生怎样的变化呢,就是由下面的算法得到.
*
* 通过e.hash & oldCap来判断节点位置通过再次hash算法后,是否会发生改变,如
* 果为0表示不会发生改变,如果为1表示会发生改变。到底怎么理解呢,举个例子:
* e.hash = 13 二进制:0000 1101
* oldCap = 32 二进制:0001 0000
* &运算: 0 二进制:0000 0000
* 结论:元素位置在扩容后不会发生改变
*/
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
/**
* e.hash = 18 二进制:0001 0010
* oldCap = 32 二进制:0001 0000
* &运算: 32 二进制:0001 0000
* 结论:元素位置在扩容后会发生改变,那么如何改变呢?
* newCap = 64 二进制:0010 0000
* 通过(newCap-1)&hash
* 即0001 1111 & 0001 0010 得0001 0010,32+2 = 34
*/
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
/**
* 若(e.hash & oldCap) == 0,下标不变,将原表某个下标的元素放到扩容表同样
* 下标的位置上
*/
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
/**
* 若(e.hash & oldCap) != 0,将原表某个下标的元素放到扩容表中
* [下标+增加的扩容量]的位置上
*/
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}