目录
一、几个重要的变量
二、HashMap扩容方法resize()分析
三、启发
1.默认初始化容量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
默认的初始化容量大小 - 必须是2的幂。1.8版本初始容量为【16】,即初始数组大小为16。
2.最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30;
哈希表数组的最大容量,即自动扩容时的容量上限。即使是使用显式构造函数指定更大的容量值,实际情况也不会超过该值。该最大容量可以通过显式构造函数指定。
3.默认负载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当前容量 * 负载因子 = 当前负载量
如果当前的存储的k-v键值对数量超过【当前负载量】,就会触发哈希表数组扩容,容量变为原来的2倍。该负载因子可以通过显式构造函数指定:HashMap(int initialCapacity, float loadFactor),源码如下:
/**
* Constructs an empty HashMap with the specified initial
* capacity and load factor.
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);
}
==>> 变量定义源码如下:
/**
* The default initial capacity - MUST be a power of two.
* 默认的初始化容量大小 - 必须是2的幂。1.8版本初始容量为16,即初始数组大小为16。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
扩容时最重要的问题是:旧数据重新分配存储位置。下图为哈希数组存储示意图(这里只分析链表形式):
HashMap存储数据过程中遇到哈希冲突时,就会产生链表。哈希数组扩容时,所有数据(数组中的数据和所有链表中的数据)都需要重新分配存储位置,方法就是hash取模。
小白:啊?还要重新分配啊?数据量级较大时,一定很耗时吧
HashMap:因为我是按照2倍扩容的,一定范围内,当数据越多,遇到扩容的几率越小,所以不必过分担心啦~
小白:哦,链表中的节点也要重新分配,可不可以为整条链表分配位置呢?这样还节省时间。
HashMap:不行的!不行的!因为原来分配的位置已经不能满足扩容后的寻址规则了呀,对hash取模时的数组长度发生了变化。懂?
小白:那样会导致什么结果呢?
HashMap:会导致找不到数据!或者取到错误的数据。总之,会出大乱子!!!
小白:那链表中的数据按什么规则重新分配呢?也像数组中的节点那样重新hash取余定位吗?
HashMap:重新hash取余定位当然可以。但是这里有个更加巧妙高效的方法。
小白:快说快说...
HashMap:就是直接将hash值与原数组容量值(注意:它是2的n次幂)做“与”运算,然后判断是否为0。这实际上就是判断hash与原数组容量的商是否为偶数。是偶数的,分到一条链表a上;是奇数的分到一条链表b上。(为什么?)链表a还放在原来索引位置上,链表b放在原索引偏移oldCap(原数组容量)的位置。(这又是为什么?)
下面我们来思考扩容前后,两个模(余数)之间存在什么关系:
对同一个hash值,做如下证明:
设:扩容前数组容量为a时:
m:商(整数)
n:余数(整数)
则 m * a + n = hash ; n范围是[0,a)扩容后数组容量为2a时:
y:商(整数)
z:余数(整数)
则 y * 2a + z = hash ; z范围是[0,2a)m * a + n = y * 2a + z
m * a + n = 2y * a + z
得到 (m - 2y) * a = z - n所以,两余数之差z-n必定是a的整数倍,
又因为n范围是[0,a),z范围是[0,2a),所以,z-n只能为0或a。
结论:扩容前后,分别对hash取模,两个模要么相等,要么相差一个原数组容量oldCap。
到这,明白上面两个“为什么”了吧,好,我们来看看源码具体是怎么写的吧(看看我们能不能得到些许启发):
/** JDK1.8
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//【当超过最大容量时,不会再扩容,直接返回原node数组】
if (oldCap >= MAXIMUM_CAPACITY) {
//【扩容阈值设置为int的最大值】
threshold = Integer.MAX_VALUE;
return oldTab;
}
//【如果原数组容量默认初始化容量,且2倍容量没有超过最大容量限制,则扩容到原来的2倍】
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
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;
//【扩容阈值 = 负载因子 * 容量】
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[] newTab = (Node[])new Node[newCap];
table = newTab;
//================ 【华丽的分割线 - 好戏在后头】 ================//
//【如果原数组非空,则进入真正的扩容逻辑;否则,无需扩容,创建数组即可】
if (oldTab != null) {
//【遍历原数组每个节点,每个节点都可能是一个链表或者一棵树,主要是解决哈希映射索引时的冲突问题】
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//【如果该节点没有后续节点(即此条链表或者树只有一个头节点),直接分配坐位(数组索引)】
if (e.next == null)
//【按照哈希值取余方法,分配坐位号(索引)】这个取模方法,妙啊~~
newTab[e.hash & (newCap - 1)] = e;
//【判断是否是树存储形式......】
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
//【链表存储形式:需要重新分配存储位置】
else { // preserve order
//低位链表
Node loHead = null, loTail = null;
//高位链表
Node hiHead = null, hiTail = null;
Node next;
//【遍历该节点对应的链表,根据哈希值确定每个节点是否】
do {
next = e.next;
//【哈希值和原数组容量做“与”运算,由于数组容量是2的n次幂,
// 所以等价于 (int)(e.hash/oldCap)%2 == 0
// 即hash中包含奇数个or偶数个原数组容量值,
// 是偶数的分到低位链表中,是奇数的分到高位链表中】妙啊~~
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;
}
//【高位链表放到新数组中的“原索引+原数组容量”的位置】
if (hiTail != null) {
hiTail.next = null;
//妙啊~【注意:为什么是偏移原数组大小oldCap呢?】
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
1.使用了好多“位与”运算和“左移”运算,取模求余从未出现“%”。
2.这个【2的n次幂】用的简直不要太棒,极大的方便了计算,更方便了位运算的使用。
思考:如果没有使用【2的n次幂】这个规则,还能使用位运算取模吗?
===== 源码版本:JDK1.8 =====