系列文章目录
文章目录
一、位运算
在学习HashMap源码之前先熟悉一下位运算:
Java提供的位运算符有:左移( << )、右移( >> ) 、无符号右移( >>> ) 、位与( & ) 、位或( | )、位非( ~ )、位异或( ^ ),除了**位非( ~ )**是一元操作符外,其它的都是二元操作符。
负数转换为二进制:按照正数转换之后按位取反,最后加一。
- 左移(<<):低位补0。
首先会将5转为2进制表示形式(java中,整数默认就是int类型,也就是32位):
0000 0000 0000 0000 0000 0000 0000 0101 然后左移2位后,低位补0:
0000 0000 0000 0000 0000 0000 0001 0100 换算成10进制为20
- 右移(>>):正数高位补0,负数高位补1
首先会将5转为2进制表示形式(java中,整数默认就是int类型,也就是32位):
0000 0000 0000 0000 0000 0000 0000 0101 然后左移2位后,低位补0:
0000 0000 0000 0000 0000 0000 0000 0001
- 无符号右移( >>> ):
正数换算成二进制后的最高位为0,负数的二进制最高为为1。
-5转换为二进制:1111 1011;
05>>>3结果是: 0001 1111;
通过其结果转换成二进制后,我们可以发现,正数右移,高位用0补,负数右移,高位用1补,当负数使用无符号右移时,用0进行部位(自然而然的,就由负数变成了正数了)
- 位与( & ):都为1则为1否则为0.
例子: 5&4
1101
0100
结果为:0100 - 位非( ~ ):一元操作符,1变成0,0变成1。
例子:~5
0000 0101 转换为:
1111 1010 - 位或( | ):有一个为1则为1,反之为0。
例子: 5|4
1101
0100
结果为:1101 - 位异或( ^ ):两个数不相等则为1,否则为0。
例子:5 ^ 3
0000 0101
0000 0011
结果:0000 0110
二、四个构造方法
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
1、无参构造方法。
2、一个参数初始容量构造方法
3、两个参数初始容量,负载因子构造方法
4、入参map构造方法,使用默认的负载因子(0.75)。
着重讲一下两个参数的构造方法:
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);
}
tableSizeFor方法
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为10,则tableSizeFor返回值是16。
注意 tableSizeFor方法返回的值是初始化容量,而并非初始化容量*负载因子得到的真正的扩容阈值,这是因为初始化阈值放在了put方法中进行初始化了。
**【重点】**下面分析tableSizeFor方法的返回值为什么是16:
当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。
首先为什么cap - 1,如果不减去1的话,当initialCapacity为2的幂的话,则最后计算的值会大一倍,具体原因请看下面的分析。
第一次右移
n |= n >>> 1;
由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。
第二次右移:
n |= n >>> 2;
注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。
第三次右移:
n |= n >>> 4;
这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。
以此类推
注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就32个1(但是这已经是负数了。在执行tableSizeFor之前,对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(2 ^ 30),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(2 ^ 30),会执行移位操作。所以这里面的移位操作之后,最大30个1,不会大于等于MAXIMUM_CAPACITY。30个1,加1之后得2 ^ 30) 。
举一个例子说明下吧。
参考连接
三、put方法
简析:
1、tab为空,说明没有初始化,则进行初始化数组。
2、tab不为空,数组长度与hash取模的位置为空,则在该位置插入键值对。
3、到这儿可能产生了哈希冲突,如果插入的key在数组中存在则直接替换,下一步进入步骤6
4、如果p是树结构,则由红黑树来处理。
5、确定p是链式结构,则遍历链表,在尾节点插入键值对。如果链表长度大于等于8则发生树变,链式结构转换为树结构。
6、如果在链表中要插入的key存在,则直接替换,并返回旧的value,否则其他情况都返回null。
7、桶的数量增加,判断是否需要扩容。
流程图如下所示:
1.1代码块一:put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
1.2代码块二:putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
1、初始化map的的节点数组
Node<K,V>[] tab;
1.1、当前key对应的节点
Node<K,V> p;
1.2、n:tab的长度,i:要插入的位置。
int n, i;
2、tab为null,length为0则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
2.1、i = (n - 1) & hash:计算要插入索引的位置,若该索引为null则直接插入键值对
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
3.1、发生碰撞,即当前位置已经存在值了
3.2、e临时节点,起到交换作用
Node<K,V> e; K k;
3.3、如果该位置原key与要插入的key相同,则直接更新键值对
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
3.4、如果p是树结构,红黑树来处理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
3.5、循环直到遍历到尾节点,设置键值对
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
3.6、 如果长度大于等于8时候,进行树变,转换为红黑树
对于临界值的分析:
假设此次是第六次,binCount == 6,不会进行树变,当前链表长度是7;下次循环。
binCount == 7,条件成立,进行树变,以后再put到这个桶的位置的时候,这个else就不走了,走中间的那个数结构的分叉语句啦
这个时候,长度为8的链表就变成了红黑树啦
if (binCount >= TREEIFY_THRESHOLD - 1) -1 for 1st
treeifyBin(tab, hash);
break;
}
3.7、当前链表key相同,结束循环,就是没有走到链表的结尾,已经找到相同的key则直接跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
这个就是p.next也就是e不为空,然后,还没有key相同的情况出现,那就继续循环链表,
p指向p.next也就是e,继续循环,继续,e=p.next
p = e;
直到p.next为空,添加新的节点;或者出现key相等,更新旧值的情况才跳出循环。
}
}
只有更新的时候,才走这,才会直接return oldValue
if (e != null) { existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
HashMap发生结构变化,变化次数累加,用于Fail-Fast机制进行判断是否抛出异常。
++modCount;
if (++size > threshold)
个数到达阈值,进行扩容
resize();
afterNodeInsertion(evict);
此处返回null是因为链表新增了节点,所以上一次的值必然为null
return null;
}
四、resize()扩容机制
简析:
1、首先认识初始的几个变量
oldCap:原来数组的长度
oldThr:扩容的阈值
newCap:新数组的长度
newThr:新扩容的阈值
2、如果oldCap>0,说明已经初始化过了,先判断是否超过最大值,则不扩容,反之数组大小和阈值都扩容一倍。
3、或构造方法指定了大小则直接设置,否则使用默认的默认 16,阈值为12。
4、最后创建新的长度的数组,旧数组数据转移到新的数组中。
2.1代码块一
final Node<K,V>[] resize() {
1.1、原数组节点
Node<K,V>[] oldTab = table;
1.2、原数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
1.3、要扩容的阈值(数组长度*负载因子如:16*0.75=12)
int oldThr = threshold;
1.4、newCap:新的数组长度,newThr:新的扩容阈值
int newCap, newThr = 0;
2.1、如果已经初始化过数组的长度
if (oldCap > 0) {
2.2、原数组长度大于等于最大值,则不进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
2.3、设置扩容的阈值为2^31-1
threshold = Integer.MAX_VALUE;
return oldTab;
}
2.4、数组长度没有超过最大值&&阈值为2的倍数,新数组扩容1倍,阈值也扩容1倍
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
3.1、构造函数设置了初始值,以该值进行初始化
newCap = oldThr;
else { // zero initial threshold signifies using defaults
3.2、构造函数没有设置初始值,则以默认值的进行初始化
newCap = DEFAULT_INITIAL_CAPACITY; //16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //12 = 16*0.75
}
3.4、构造函数设置了默认值,设置扩容阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
3.5、设置新的扩容阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
3.6、创建新容量的node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
4、如果旧数组为空,则在进行初始化,反之遍历旧数组,把数据移动到新的数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
5.1、当前节点的值不为空
if ((e = oldTab[j]) != null) {
5.2、把原数组当前位置设置为null,帮助垃圾回收
oldTab[j] = null;
5.3、e.next为null说明不是链式结构和树结构,直接设置到新数组中
if (e.next == null)
5.4、取模运算计算数组的索引位置
newTab[e.hash & (newCap - 1)] = e;
5.5、e是树形结构
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
6.1、该节点为链式结构 ,lo链表 和 hi链表, loHead 和 loTail 分别指向 lo链表的头节点和尾节点, hiHead 和 hiTail以此类推.
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
6.2、遍历单向链表,如果 (e.hash & oldCap) == 0则旧数组索引与新数组索引不变
否则,新数组索引为原数组索引+oldCap(原数组大小)
next = e.next;
if ((e.hash & oldCap) == 0) {
//如果尾节点为null,说明还没有数据
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); //单链表循环结束的条件
//如果 (e.hash & oldCap) == 0 则该节点在新表的下标位置与旧表一致都为 j
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果 (e.hash & oldCap) == 1 则该节点在新表的下标位置 j + oldCap
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
五、getNode方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
1、tab是否为空&&长度是否大于0&&在当前key索引的位置的value是否为空
2、(n - 1) & hash就求当前key在数组中索引的位置的
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
3、如果在索引位置的key与入参的key相同则直接返回。
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
4、说明是链式结构,可能是链表或者红黑树
if ((e = first.next) != null) {
4.1、是红黑树,则在红黑树中获取节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
4.2、遍历单向链表找到对应的key,返回对应的value
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}