HashMapt源码解析
1.HashMap介绍
HashMap作为Map的主要实现类,是存储K-V键值对的数据结构,可以存储null的key和value,底层用数组+链表+红黑树的存储,也叫哈希桶
HashMap是线程不安全的,
在jdk 1.8之前都是数组+链表的结构,在链表中的查询操作都是O(N)的时间复杂度
为了提高效率,1.8之后改为数组+链表+红黑树,当链表节点数量达到一定值,链表转换为红黑树结构,增删改查都是O(log n)。
HashMap存储过程:
当调用HashMap的构造函数,先对相关属性初始化,在第一次map.put(key1,value2)时对散列表进行初始化
首先,使用hash()计算key1的哈希值,哈希值经过路由算法(table.length-1)^hash计算出元素在数组中的存储位置
情况一:如果此位置上的数据为空,此时的key1-value1直接添加;
情况二:如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式或红黑树形式存在)),遍历该桶里的节点
1.如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。
2.如果key2的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
2.1如果equals()返回false:此时key1-value1添加成功。
2.2如果equals()返回true:使用value1替换value2。
再添加过程中如果容量达到了临界值(且要存放的位置非空),则使用resize()进行扩容,扩容长度必须是2^n且默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 或当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
2.HashMap源码分析
2.1 属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//序列版本号
private static final long serialVersionUID = 362498820763181265L;
//默认初始容量2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表结点转换为红黑树的阈值:当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
static final int TREEIFY_THRESHOLD = 8;
//红黑树节点转换为链表节点的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//转换为红黑树时table的最小程度
static final int MIN_TREEIFY_CAPACITY = 64;
//存储K-V键值对的数组,K-V封装在Entry类中
transient java.util.HashMap.Node<K,V>[] table;
//保存缓存entrySet()
transient Set<Map.Entry<K,V>> entrySet;
//table中元素个数
transient int size;
//当前hash表结构修改次数
transient int modCount;
//table数组扩容的阈值
//若没有指定负载因子:threshold=table.length*0.75,
//指定了则threshold=table.length*loaderFactor,当哈希表中元素或长度超过阈值时,触发扩容
int threshold;
//加载因子
final float loadFactor;
}
-
为什么红黑树的效率比链表高,树化以后查找和插入最差时间复杂度为 O(logN),为什么不一开始就用红黑树
红黑树插入元素和删除元素,要左旋右和反转颜色来平衡树,这也是要性能的,元素少时不值得 -
为什么需要扩容
为了解决hash冲突导致的链化影响查询的效率,扩容会缓解该问题 -
初始容量是16,为什么是2的指数次幂?
因为要保证key的值尽量不重复,在计算索引时方便使用位运算,用key的hashcode值的高低16位做异或运算(得出hash值,)再与数组的长度-1的二进制做与运算,得出index。
如果数组长度的二进制是0的话,key的hashcode值的每一位无论是1还是0做与运算都是0,这样重复的概率就变大,因此必须都是1。而
1111+1=1 0000 就是2^4
1 1111+1=10 0000 2^5,结果都是2的N次方
2^N - 1可以使二进制每位都是1 -
加载因子为什么是 0.75f
默认加载因子 0.75 在时间和空间开销上给出了一个较好的折衷。
如果加载因子过大,空间开销少了,但会导致查找元素效率低;而过小则会导致空间开销大,数组利用率低,分布稀疏。
2.2节点类
- Node节点
HashMap中的静态内部类,继承了Map.Entry(),Entry中封装了K-V对,内部类能很方便的使用类中的属性和方法而不用new对象
//HashMap中的k-v键值对封装在Node内部类中
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//节点中K的哈希值
final K key;//key
V value;//value
java.util.HashMap.Node<K,V> next;//连接的下一节点
//Node构造函数
Node(int hash, K key, V value, java.util.HashMap.Node<K,V> next) {
。。。。。
}
。。。。。
}
- 树节点:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; //父亲节点
TreeNode<K,V> left; //左节点
TreeNode<K,V> right; //右节点
TreeNode<K,V> prev;
boolean red; //结点颜色(红、黑)
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
。。。。。
}
2.3 构造函数:
1)无参构造函数
public HashMap() {
// 无参构造函数,加载因子使用默认值 0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
2)有参构造函数-参数:参数指定了初始容量
public HashMap(int initialCapacity) {
//设置初始容量,并使用默认的加载因子,实际调用HashMap(int initialCapacity, float loadFactor)
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3)有参构造函数-参数指定了初始容量与加载因子
public HashMap(int initialCapacity, float loadFactor) {
//首先检查要设置的容量与加载因子是否合法
//1.如果小于0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//2.如果传入的参数大于最大容量,则将容量设置为MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果加载因为<0,或过大,则抛出异常
//Java.lang.Float.isNaN()方法 :此方法如果此对象所表示的值是NaN,返回true,否则返回false。
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//由于数组的容量必须是2的次幂【为了后面方便计算元素的位置】,需要通过tableSizeFor()计算出>=指定容量的最小2次幂数
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor(initialCapacity)
返回一个大于等于cap的一个数组,并且这个数字一定是2的次方数
在上面种为什么不直接用this.threshold = initialCapacity,而是用tableSizeFor()计算?为了保证散列表容量为2^n
static final int tableSizeFor(int cap) {
//cap做减1,是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍,如传入的是8,0000 1000经过右移 0000 1111 ,最后+1就是0001 0000
/*
如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)
如果n不等于0,则其中某一位一定是1,如0001 ****** ,在不断轮循右移后在于原值做或,最后原始为1后的数字全变成1,得到
0000,最后+1也就得到了最小的大于指定容量的n^2*/
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
/* 容量最大也就是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)
*/
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
上述三个构造方法其实是对初始容量和加载因子进行设置,初始容量必须是2的指数次幂,在第一次put操作时才对数组容量进行初始化
4) 有参构造-参数Map集合
public HashMap(Map<? extends K, ? extends V> m) {
//将loadFActor初始化为0.75,并用putMapEntry()将形参集合中的元素加载进来
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//首先获取形参map的元素个数
int s = m.size();
//1.如果形参map 长度大于0
if (s > 0) {
//1.1如果table为空,
if (table == null) {
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
//1.2如果数组长度>当前数组的临界值,则扩容
else if (s > threshold)
resize();
//遍历形参map元素,用putVal()假如到当前数组中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
3.方法介绍
1)定位
先通过hash()将key哈希值计算出来,再通过(table.length - 1) & hash计算key在散列表中的索引位置
-
hash()
Hash也称散列、哈希,由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。
hash值的计算可以定义不同的规则,但hash算法的冲突概率要小,计算出的hash值应该尽可能的元素散列存储
//让hash值的低16位也具有高16位的特征,以减小数组长度较小时的hash碰撞
//异或:相同返回0,不同返回1
/*
eg:
hash = 0b 0010 0101 1010 1100 0011 1111 0010 1110
h>>>16 = 0b 0000 0000 0000 0000 0010 0101 1010 1100
& 0b 0010 0101 1010 1100 0001 1010 1000 0010
这样避免了如果table.length小,hash数值很大且末尾都是零的情况下,计算元素索引位置,即hash^(table.length-1),大量元素冲突
,扰动后可以让这些数值更加分散,减少哈希冲突
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- (table.length - 1) & hash:计算元素位置
通过hash计算出key的hash值,再用(table.length - 1) & hash
将hash与n-1异或,相当于hash%数组哈希表长度,将位置控制在了数组长度范围内
如hash=25 散列表长度为16-1=15
0001 1001
& 0000 1111
0000 1001
即高位变为0,保留低位,从而保证了hash值分布在1数组合法位置
2)扩容resize():
扩容会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,非常耗时,因此需要尽量避免rehash。
//计算扩容后的新容量和阈值
final Node<K,V>[] resize() {
//oldTab:引用扩容前的哈希表
Node<K,V>[] oldTab = table;
//oldCap:表示扩容之前table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length; // table如果为空,oldCap长度设置为0
//oldThr:表示扩容之前的扩容阈值,触发本次扩容阈值
int oldThr = threshold;
//newCap:扩容之后table数组的大小
//newThr:扩容之后,下次再次触发扩容的条件
int newCap, newThr = 0;
// 1.oldCap容量大于0,即hashMap中的散列表已经初始化了,是一次正常的扩容
if (oldCap > 0) {
//1.1如果扩容之前的table容量已经达到了最大值,则不扩容,且设置扩容条件阈值为int最大值,并返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
/* 1.2 oldCap左移一位实现数值翻倍,并且赋值给newCap,newCap小于数组最大限制,扩容前阈值>=16
*该情况则下一次扩容的阈值等于当前扩容的阈值的翻倍
oldCap < DEFAULT_INITIAL_CAPACITY情况,即调用的是指定初始化容量的HashMap的构造函数
如传入的容量大小是8,则不用更新newThr
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)-----------------------------情况1
// 将阈值threshold*2得到新的阈值
newThr = oldThr << 1;
}
//2.oldCap等于0,说明hashMap中的散列表为null
/*
2.1 oldThr>0,说明使用的构造方法是
HashMap(initialCap, loadFactor)-->this.threshold = tableSizeFor(initialCapacity);
HashMap(initialCap)-->threshold=DEFAULT_LOAD_FACTOR
HashMap(map),并且map中有数据-->
this.loadFactor =DEFAULT_LOAD_FACTOR;
float ft = ((float)s / loadFactor) + 1.0F;
threshold = tableSizeFor(t);
该方法中 this.threshold = tableSizeFor(initialCapacity);
上述的threshold使用tableSizeFor方法返回的是数组的容量(2^N),例如initialCapacity是1000,那么得到的threshold就是1024,这里threshold就等于数组的容量
float ft = ((float)s / loadFactor) + 1.0F;
threshold = tableSizeFor(t);
*/
else if (oldThr > 0) -----------------------------------------情况2
// 容量设置为阈值newCap容量为旧阈值,都为2^n
newCap = oldThr;
else {
// 2.2 oldThr=0,说明使用的是无参构造函数HashMap(),loadFactor =DEFAULT_LOAD_FACTOR;只设置了加载因子 阈值初始化时的0
// 将新的长度设置为默认的初始化长度,即16
newCap = DEFAULT_INITIAL_CAPACITY;
// 负载因子0.75*数组长度16=12 新阈值为12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/* 3.如果新阈值为0,即上面标明的情况1,情况2
根据负载因子设置新阈值
*/
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]; // 创建一个长度为newCap的新的Node数组
table = newTab;
// 如果旧的数组中有数据,则将数组复制到新的数组中
if (oldTab != null) {
// 循环遍历旧数组,将有元素的节点进行复制
for (int j = 0; j < oldCap; ++j) {
//e:临时变量,表示当前节点
Node<K,V> e;
// 说明当前桶中有数据,该桶中可能只有单个数据,或者链表,或者红黑树
if ((e = oldTab[j]) != null) {
//如果ObjTab[j]存储为空,则将他变成空引用,方便JVM 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 {
//低位链表:存放在扩容之后的数组的下标位置,与当前数组的下标位置一致
Node<K,V> loHead = null, loTail = null;
//高位链表:存放在扩容之后的数组的下标位置为 当前数组下标位置 + 扩容之前数组的长度
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// hash值与旧的长度做与运算用于判断元素的在数组中的位置是否需要移动
/**
* 举例:
* (e.hash & oldCap) == 1
* e.hash & (oldCap - 1) e.hash & (newCap - 1) e.hash & oldCap
* ...0101 0010 ...0101 0010 ...0101 0010
* & 0 1111 1 1111 1 0000
* 0 0010 1 0010 1 0000
*
* (e.hash & oldCap) == 0
* e.hash & (oldCap - 1) e.hash & (newCap - 1) e.hash & oldCap
* ...0100 0010 ...0100 0010 ...0100 0010
* & 0 1111 1 1111 1 0000
* 0 0010 0 0010 0 0000
*/
// 总结:
// 数组的长度为2^N,即高位为1,其余为0,计算e.hash & oldCap只需看oldCap最高位1所对应的hash位
// 因为newCap进行了双倍扩容,即将oldCap左移一位,那么oldCap-1相当于newCap-1右移一位,右移后高位补0,与运算只能得到0。
// 如果(e.hash & oldCap) == 0,hash值需要与运算的那一位为0,那么oldCap - 1与newCap - 1的高位都是0,其余位又是相同的,表明旧元素与新元素计算出的位置相同。
// 同理,当其 == 1 时,oldCap-1高位为0,newCap-1高位为1,其余位相同,计算出的新元素的位置比旧元素位置多了2^N,即得出新元素的下标=旧下标+oldCap
// 如果为0,元素位置在扩容后数组中的位置没有发生改变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 首位
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 不为0,元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长
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;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3)put()
1.先用找到要插入元素key存放的位置
2.1如果定位到的数组位置没有元素就直接插入;
2.2如果定位到的数组位置有元素就要与插入的key比较
如果key相同就直接覆盖
如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入;如果不是就遍历链表尾部插入。
//向HashMap中添加元素,实际调用了putVal()
public V put(K key, V value) {
//传入的第一个参数为key的hash值,通过调用hash()计算出
return putVal(hash(key), key, value, false, true);
}
// 参数onlyIfAbsent:表示如果散列表中已经存在某个key与要插入的key相同,则不插入该元素
// evict:无须管
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
/*
tab:引用当前hashMap的散列表
p:表示当前散列表的元素
n:表示散列表数组的长度
i:表示路由寻址结果
*/
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果table为空或长度为0,则用resize()进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//1.通过定位算法计算元素在数组中的存放位置,如果当前位置没有元素,则将元素放在该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//2.若给位置已经存在元素:该桶可能是链表或红黑树
else {
//e:临死节点:e!=null,
//k:临时key
Node<K,V> e; K k;
//2.1如果头元素和插入元素两者key的hash一样,且通过equals()它两key相同,则记录下该节点到p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 2.2如果该节点为红黑树结点
else if (p instanceof TreeNode)
//则调用红黑树的插入元素方法putTreeVal(),找到要插入的位置节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//2.3 其余情况表示给桶结构为链表
else {
// 遍历当前链表,并记录节点数
for (int binCount = 0; ; ++binCount) {
// p.next为空表明处于链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 插完后判断结点数量,如果达到阈值8(为什么阈值-1,binCount>=7时转红黑树,因为这里是先插入,在判断,从0开始,前面总共8个数据,插入第九个时,然后树化)
if (binCount >= TREEIFY_THRESHOLD - 1)
// 转化为红黑树
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 通过上述操作找到e后,e有记录,表示在桶中找到的元素的key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e(第一个元素)的value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调,将元素添加到链表的最后
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 表示散列表结构被修改的次数,替换Node元素的value不计算
++modCount;
// 每次put一个元素++size,当实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
4) get
//获取元素
public V get(Object key) {
Node<K,V> e;
//通过getNode()获取指定键的值,如果未找到,则为null,否则为value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
/*fist:桶位中的的头元素
*tab:引用当前hashmap的散列表
*e:临时node元素
*n:table数组长度
*/
//如果table不为null,且table中有元素且当前key所在位置有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//1.如果查找的key的hash与第一个元素hash一样且,key相同,则找到直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//2.如果第一个节点后后有元素,可能是链表,也可能是红黑树
if ((e = first.next) != null) {
//2.1该结构为红黑树,则调用getTreeNode()查找红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//2.2该结构为链表,则通过do-while循环挨个查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
5) remove()
public V remove(Object key) {
Node<K,V> e;
//通过removeNode()执行删除操作,如果不为null,返回删除节点的value,否则返回null
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
public boolean remove(Object key, Object value) {//删除指定K-V的节点
return removeNode(hash(key), key, value, true, true) != null;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab:引用当前hashMap中的散列表
//p:当前node元素
//n:表示散列表数组长度
//index:表示寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果table不为null,且table中有元素且当前key所在位置不为空,则进行查找和删除
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node:要查找的结果
//e:当前Node的下一个元素
Node<K,V> node = null, e; K k; V v;
//1:当前桶位中的元素即为你要删除的元素,则记入node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//2:当前桶位不止一个元素,即可能是红黑树树结构,或链表结构
else if ((e = p.next) != null) {
//2.1如果是红黑树,则用getTreeNode()查找,记入node
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//2.2如果是链表的情况,则通过do-while循环挨个查找
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//查找到要删除的node后,如果node不为空,且node的value等于传入的value,则执行删除逻辑
//matchValue:匹配结果,其从形参传过来的是true
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果要删除的节点是树节点,则调用红黑树removeTreeNode()的删除逻辑
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//如果桶的第一个元素即为要删除的元素,则直接覆盖
else if (node == p)
tab[index] = node.next;
//如果为链表,则 p.next = node.next;
else
p.next = node.next;
//记录操作数,元素个数size--
++modCount;
--size;
afterNodeRemoval(node);
//返回删除的节点
return node;
}
}
return null;
}
6) replace()
//更新指定key的value值
public V replace(K key, V value) {
Node<K,V> e;
//调用getNode()找到指定的节点,更新value
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
//调用getNode()找到指定的节点并判断value是否与oldValu相同,相同则更新为newVaule
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
7.balanceInsertion()
//红黑树插入元素节点的核心逻辑
/**
*
* @param root 当前红黑树的空节点
* @param x 要插入到红黑树中的新节点
* @param <K>
* @param <V>
* @return
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//新节点默认是红色
x.red = true;
//xp:x父节点 xpp:x祖父节点 xppl:xpp的左孩子 xppr:xpp右孩子
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//1.如果x父节点为空,则x是第一个节点,自动为根节点,根节点为黑色
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//2.如果父节点为黑色,或者x没有祖父节点,即x是第层节点,父节点为根节点,该情况则无需操作
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//3.如果父节点为红色
//3.1 如果x的父节点是祖父节点的左孩子
if (xp == (xppl = xpp.left)) {
//3.1.1 祖父节点的右孩子不为空且是红色
if ((xppr = xpp.right) != null && xppr.red) {
//父节点和叔叔节点都为红色,则父节点+叔叔节点变黑,祖父节点变红,在从祖父节点开始进行递归调整
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
//3.1.2 没有叔叔节点,或叔叔节点是黑色
else {
//如果x节点是其父亲节点的右孩子,则用rotateLeft()进行左旋
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果x节点是其父亲节点的是左孩子或者x是上述经过左旋的操作后的树,直接rotateRight()进行右旋,
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
//3.2 如果x的父节点是祖父节点的右孩子
else {
//3.2.1 祖父节点的左孩子不为空且是红色
if (xppl != null && xppl.red) {
//父节点和叔叔节点都为红色,则父节点+叔叔节点变黑,祖父节点变红,在从祖父节点开始进行递归调整
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
//3.2.2 没有叔叔节点,或叔叔节点是黑色
else {
//如果x节点是其父亲节点的左孩子,则用rotateRight()进行左旋
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
如果x节点是其父亲节点的是右孩子或者x是上述经过右旋的操作后的树,直接rotateLeft()进行左旋,
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
7.balanceInsertion()
4.小结:
- 位运算
&(与): 全1才为1
|(或): 有1就是1
^(异或):相异才为1
>>: 右移
<<: 左移
-
为什么红黑树的效率比链表高,树化以后查找和插入最差时间复杂度为 O(logN),链表挨个查找效率O(N)
-
为什么不一开始就用红黑树
红黑树插入元素和删除元素,要左旋右旋反转颜色来维持红黑树平衡,元素少时效率就比不上链表 -
为什么需要扩容
为了解决hash冲突导致的链化影响查询的效率,扩容会缓解该问题 -
初始容量是16,为什么是2的指数次幂?【使用tableSizeFor()保证容量是2^n】
方便使用位运算计算key的索引 -
加载因子为什么是 0.75f
默认加载因子 0.75 在时间和空间开销上给出了一个较好的折衷。
如果加载因子过小,扩容阈值threshold=table.length*loaderFactor,空间开销少了,但会导致查找元素效率低;而过大则会导致空间开销大,数组利用率低,分布稀疏。
暂时为解决的:
-
hashmap线程安全问题–>ConCurrentHashMap
-
红黑树的节点的增删改
-
为什么jdk7插入节点用头插法,1.8之后改为尾插