在理解扩容器前得先明白HashMap中几个常量的含义
//默认初始容量 ,必须为二的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子, 当前所占空间/总空间 ,若该值大于0.75 则需要进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//转为树的阈值(链表转为红黑树的阈值)
static final int TREEIFY_THRESHOLD = 8;
//加载因子,可自定义其大小
final float loadFactor;
接下来便是扩容操作的
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//获得当前数组容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//阈值的赋值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//容量大于等于最大容量则返回当前的tab(不再扩容任由hash冲突,再扩容数组过大。)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容前数组容量的两倍小于 最大容量 且 原来容量大于初始容量(即已经扩容过了)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容两倍, 且阈值也乘以二
newThr = oldThr << 1; // double threshold
}
//这种情况是对于 初始化定义了容量大小的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//容量初始化,当第一次新增元素时
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//这里是针对 初始化HashMap时定义了容量情况, 因为其他两种情况都对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 = newTab;
//开始对新扩容后的table赋值
if (oldTab != null) {
//遍历旧的桶中所有数据,重新计算应该将数据所放位置。(将旧的桶中数据都移动到新的桶中去)
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//将旧桶数据置空,便于GC回收
oldTab[j] = null;
//判断该节点链表(或红黑树)下一个引用是否还存在数据
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;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//计算新表后 ,元素位置
do {
next = e.next;
//这里采用了一个非常巧妙的方法,判断该节点扩容两倍后,该节点扩容位值是0或是1来判断该节点应该是在
//原为,还是移动table[j+oldCap]上去,例如当前oldCap = 16,即2^4,进行扩容后直接将节点的hash值
//oldCap & hash 得到的就是扩展位的值是0还是1了,非常巧妙
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//原索引放到新表中同样的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//原索引放到新表中同样索引位置+oldCap
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashMap扩容可以分为三种情况:
**第一种:**使用默认构造方法初始化HashMap。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。
**第二种:**指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着threshold = 当前的容量(threshold) * DEFAULT_LOAD_FACTOR。
**第三种:**HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。
tab[i = (n - 1) & hash] 方式得出元素在数组中存放位置
** **n-1 即为数组最大下标值,最大小标志与key的哈希值做与运算 , 由与运算特性可知 ,得出的结果肯定是小于两者中的最小值的。而哈希值一般是比较大的一个值,因此就相当于是取余操作。
-
那么为什么不直接用取余操作呢?
- & 运算效率远远高于 % 运算。
扩容时候,索引位置计算原则
总结:
在进行扩容时候时,可以做到各个元素存放位置变动不大,要么是原来的位置,要么是原来索引+扩容前数组总容量。极大的减少了开销同时效率,不用rehash, 只需要判断新增那位是零还是一, 零则还是原来索引位置,一则原来索引+扩容前数组总容量即可。