文章目录
一、概述
我们先来看一张图,回顾一下之前学习的ArrayList、LinkedList、HashMap
(1)数组的优势/劣势
采用一段连续的存储单元来存储数据。它是由相同类型的元素的集合所组成,并且被分配一块连续的内存来存储(与链表对比),利用元素的索引可以计算出该元素对应的存储地址。它的特点是提供随机访问并且容量有限。
缺点:java中内存申请之后大小是固定的,如果数组满了还想再插入数据,就需要重新再建一个容量更大数组,把数据导进去之后再插入新的数据,这个过程很浪费时间。
优点:查找速度很快,对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
(2)链表的优势/劣势
链表是一种线性表,不像顺序表那样连续存储元素,而是在每一个节点里存到下一个节点的指针。
优点:由于不用连续存储,对于链表的新增、删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),比顺序表快得多;
缺点:但查找操作需要遍历链表逐一进行比对,复杂度为O(n),而顺序表相应的时间复杂度分别是O(logn)和O(1)。
(3)散列表的特点
散列表,又叫哈希表(Hash Table),是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。
(4)什么是Hash
哈希:Hash
基本原理就是把任意长度输入,转化为固定长度输出
这个映射的规则就是Hash算法,而原始数据映射的二进制串就是Hash值
Hash的特点
- 1.从Hash值不可以反向推导出原始数据
- 2.输入数据的微小变化会得到完全不同的Hash值,相同的数据一定可以得到相同的值
- 3.哈希算法的执行效率要高效,长的文本也能快速计算Hash值
- 4.Hash算法的冲突概率要小
由于Hash原理就是将输入空间映射成Hash空间,而Hash空间远远小于输入空间,根据抽屉原理,一定存在不同输出有相同的映射
抽屉原理
由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。
抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。
二、HashMap原理讲解
(1)的继承体系
HashMap继承了AbstractMap,实现了Cloneable接口、Serializable接口、Map<K,V>接口
(2)Node数据结构分析
Node节点主要有四个变量:
hash:哈希值,用于路由寻址公式计算出对应的索引位置:index = (table.length - 1) & node.hash
<k,v>:键值对
next:指针
static class Node<K,V> implements Map.Entry<K,V>{
final int hash;//K.hash--->f(k.hash)(扰动函数,让hash值更散列)--->hash
final K key;
V value;
Node<K,V> next;
}
(3)底层存储结构介绍
哈希表,整体是一个数组,不过Node数组的每一项都是由一个单链表组成,下标由hash
值与数组长度与运算(路由寻址)构成。在jdk8中,当数组长度达到64(有些文章里也叫数组容量)并且单链表的长度达到8之后,链表会转化成红黑树的结构。
为什么选择8:当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值(树化门槛)。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。
(4)put数据原理分析
这幅图路由寻址公式index=(table.length - 1) & node.hash
你可能不太理解,它的意思就是index=(表的长度减一)& (哈希值)
表的长度一定是2的幂方次,本例中是16,先16减去1得到15,把15转为二进制为(0000 00000 1111),然后用这个二进制和哈希值做一下与运算得到0010,转为十进制为2,所以下标为2。
(5)什么是Hash碰撞
哈希碰撞(冲突):要把一个对象插入散列表,由路由寻址公式,计算出来的值有可能是一样的。找到这个对象应该插入散列表的位置时,已经存放了其他的对象。
原因:哈希就是做一个映射,为的是查找快。因为映射毕竟是有一个范围的,这个范围可能会小于你原来的那个范围,所以可能好多个值映射了之后成为一个值,即产生哈希冲突。
(6)什么是链化
因为哈希碰撞,插入一个对象时,就会在链表最后面插入(jdk7 头插,jdk8 尾插)。导致链表越来越长,查询会变慢,最后接近为o(n)。
(7)jdk8为什么引入红黑树
解决链化很长的情况,因为红黑树是一个自平衡的二叉查找树,查找效率比链表高。
(8)HashMap扩容原理
16位的数组桶位bin被装满的时候,离散性就变得很差,bin中链表长度会达到阈值(树化门槛),又会导致查询效率变慢。扩容即用空间换时间的思想,提高查找的性能。
三、部分方法的源码:
0.HashMap几个常量
//定义了数组默认大小是2^4
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//定义了数组最大大小是2^30
static final int MAXIMUM_CAPACITY = 1<<30;
//定义了默认的负载因子大小是0.75
static final int DEFAULT_LOAD_FACTOR = 0.75f;
//定义了树化阈值(有一个链长度大于8,并且链表所有元素之和超过64,那么链表结构就变为红黑树)
static final int TREEIFY_THRESHOLD = 8;
//定义了树化阈值(有一个链长度大于8,并且链表所有元素之和超过64,那么链表结构就变为红黑树)
static final int MIN_TREEIFY_CAPACITY = 64;
1.HashMap核心属性(threshold、loadFactory、size、modCount)
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 这就是我们的HashMap散列表表,长度始终是2的幂。
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
// 当前哈希表中元素个数
transient int size;
// 当前哈希表结构修改次数(增、删时才算结构修改)
transient int modCount;
// 负载因子就是0.75
final float loadFactor;
// 扩容阈值,当你的哈希表中的元素超过阈值时,触发扩容。
// 计算方式:threshold = capacity * loadFactor,也就是说“扩容阈值=(表的大小*0.75)”
int threshold;
}
2.HashMap的构造方法
构造方法做了什么呢?
首先人家不知道该创建一个多大的HashMap,所以构造方法先判断调用者有没有把(表的大小Capacity)和(负载因子loadFactor)传进来,
①如果传进来了就用调用者传过来的值(前提是调用者传进来的值可能不规范,我们做一系列加工处理)计算出扩容阈值+负载因子
②如果调用者只传进来(表的大小Capacity)那么(负载因子loadFactor)就使用默认的值计算出扩容阈值+负载因子
③如果调用者只传来一个Map,那么经过一系列操做计算出扩容阈值+负载因子
④如果是空参构造方法,那么只会计算出负载因子(扩容阈值为空!!!)
//构造方法
public HashMap(int initialCapacity, float loadFactor) {
//做一些校验,capacity必须大于0,最大值也就是MAX_CAP
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//lodaFactor负载因子必须大于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//调用了下面的函数
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
// 此方法核心功能就是求出“大于等于输入长度的2次幂的值”
// 如输出:8,输出为8
// 如输出:9,输出为16
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1; // n = 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;
}
3.HashMap的put方法分析
HashMap添加一个元素时,
第一步,先执行hash(Object key),把key转为对应的hashcode
第二步,执行putVal(hashcode, key, value)方法,具体如下:
先根据数组长度和hashcode执行一次路由运算 得到index,根据index找node数组中的元素,
情况①:node数组中该索引下没有元素,那太好了,我直接把k-v键值对扔进去
情况②:node数组中该索引下有元素,而且很巧的是当前桶位中的元素的key与你要插入的元素的key完全一致,那就执行替换操作;
情况③:node数组中该索引下有元素,而且当前桶位中的元素的key与你要插入的元素的key不一致,那就判断它下面是链表结构还是树结构,如果是链表,那就遍历链表,如果如果到末尾了还是没有找到这个key那就把这个k-v键值对插到最后一位。把元素添加到链表后判断如果当前链表的大小大于8个节点,就调用树化方法,但是在进行树化时里面还会判断总元素数量是否小于64,如果小于64的话就不会把他变为红黑树而是先对表进行扩容;
情况④:已经树化了,另说。
第三步,因为你插入了新元素所以在函数最后还判断了现在散列表所有元素个数是否大于扩容阈值,如果大于扩容阈值那就执行 resize(),进行扩容
另外,
我们一直在说新值替换旧值,如何做的替换呢?
它把新的值覆写到旧的值上面,顺带还把旧的值返回了(返回干嘛我现在也不知道)
它里面还有个修改次数统计:
modCount表示的是当前哈希表结构修改次数(增、删时才算结构修改),如果只做了替换覆盖那可不算
①完整代码,别看了
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
//延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//最简单的一种情况:寻址找到的桶位,刚好是null,这个时候,直接将当前k-v=>node扔进去就可以
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//e:不为null的话,找到了一个与当前要插入的key-value一致的key元素
//k:表示临时的一个key
Node<K,V> e; K k;
//表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//判断链表的情况,而且链表的头元素与我们要插入的key不一致。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表达到树化阈值长度,进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//条件成立的话,说明找到了相同key的node元素,需要进行替换
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不等于null,条件成立说明,找到了一个与你插入元素key完全一致的数据,需要进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//modCount:表示散列表结构被修改的次数,替换Node元素的Value不计数
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
// 作用:计算 key.hashCode() 并将散列的较高位(异或)传播到较低位。
// ^ 异或:相同为0,不同为1
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
②代码中hash(Object key)函数的解释(必看!)
4.HashMap resize扩容方法分析
1.为什么要扩容?
为什么扩容:如果数组被装满后,只能链化,链化非常严重最后相当于是链性查找,查询效率从O(1)—>O(n)。HashMap由node数组+链表组成,如果长度为16的node数组里面有个链表长度为8,现在把它扩容成长度为32的数组,那么那个长度为8的链表就被拆开为两个长度为4的链表了。
2.扩容机制
1.第一次添加时,table数组扩容到16,临界值(threshold)是16 乘以 加载因子(loadFactor)0.75 = 12
2.如果table数组使用到了临界值12,就会扩容到16 x 2= 32,新的临界值就是32 x 0.75= 24,依次类推
3.在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
4.如果单条链表节点超过了8但是总的节点数没达到64的话,就扩容table表(按2倍扩容)
案例:
重写HashSet的hashcode方法,让其返回一个固定在100,也就是说所有节点的hashcode都是100,那么你添加12个元素由于它们的hashcode相同所有都会被添加到同一条链上。
用Debug启动,一开始table表长度为16,
当你添加完第8个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为32,但是这8个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第9个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为64,但是这9个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第10个元素后,由于单条链表节点超过了8而且总的节点数达到64,所以该链表变为红黑树结构
3.记住一些变量:
oldTab:扩容前的哈希表
oldCap:扩容前table数组的长度
oldThr:表示扩容之前的扩容阈值(threshold),也就是触发本次扩容的阈值
newCap:扩容之后table数组的大小
newThr:下次再次触发扩容的条件
4.回顾HashMap 的四种构造方法
新建一个HashMap 有四种构造方法
1.new HashMap(int initialCapacity, float loadFactor);
2.new HashMap(int initialCapacity);
3.new HashMap(Map);
4.new HashMap();
前三种扩容阈值和负载因子均不为空
,第四种负载因子loadFactor不空但扩容阈值threshold为空
5.扩容的具体过程:
第一步,先计算newCap与newThr
①oldCap > 0 说明扩容前的哈希表大于0,表示hashMap散列表已经初始化过,是正常扩容。那就继续判断:
如果扩容之前的table数组已经大小达到最大容量后,则不扩容,且设置扩容条件threshold为Integer.MAX_VALUE(一个无穷大的数);
如果扩容之前的table数组没有达到最大扩容,那就扩容翻倍,如何扩容翻倍呢?用位运算,让oldCap左移一位就实现了翻倍(newCap = oldCap << 1),同时newThr = oldThr << 1也就是说下次再次触发扩容的条件也翻倍了。
总结:oldCap > 0 ,要么不扩容,要扩容的话newCap = oldCap << 1而且newThr = oldThr << 1
补充:
上面分析了oldCap > 0 大于0的情况,
而如果oldCap = 0说明hashMap中的散列表是null,散列表是null我们还要判断扩容阈值是不是空的。
②oldCap = 0时,如果oldThr > 0,那么设置newCap设置为oldThr的大小,设置newThr的大小为(nweCap)乘以(负载因子loadFactor)
补充:什么时候就会出现oldThr > 0但是oldCap = 0的情况呢?
由于oldThr=threshold,oldThr > 0说明threshold > 0,在使用前三种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold > 0的HashMap,构造完HashMap还没有初始化所以就出现了oldCap = 0且oldThr > 0的情况了
③oldCap = 0时,如果oldThr = 0,那么newCap设置为默认的数组大小(16)
,newThr设置为“(16*0.75=12)”
补充:什么时候就会出现oldCap = 0而且oldThr = 0的情况呢?
在使用第四种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold为空的HashMap,构造完HashMap还没有初始化所以就出现了oldCap > = 0且oldThr = 0的情况了
第二步:进行扩容
新索引要么与原索引相同要么就是原索引加上原数组的长度得到新索引。
先创建一个更大更长的哈希表,
如果扩容前的哈希表不为空,首先根据index找到对应的桶,
①桶里面如果有数据,那就判断桶的下一位是否为空,如果为空那说明桶里面就它一个元素,那就使用寻址算法找到该元素在新的哈希表的index,然后设置进去;然后把旧的哈希表里面的桶元素置空(让JVM去回收)。
②桶里面如果有数据,桶的下一位不为空而且桶里面的数据已经树化,那就按照树的方式去处理;
③桶里面如果有数据,桶的下一位不为空而且桶里面的数据还是链表,那就开始寻址了:
因为table是2倍扩容,所以只需要看当前元素的hash值与扩容前数组的长度做"与运算",结果为0,那么还是原来的index;否则index = index + oldCap;
(下面会解释这句话)
我们知道15号桶的hash值的后五位要么是1 1111,要么是0 1111
假如它的hash值的后五位是1 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为1,那么它现在的索引就是15+16=31;
假如它的hash值的后五位是0 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为0,那么它现在的索引还是15;
6.完整代码:(不用看)
final Node<K,V>[] resize() {
// oldTab:引用扩容前的哈希表
Node<K,V>[] oldTab = table;
// oldCap:引用扩容前table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr:表示扩容之前的扩容阈值,触发本次扩容的阈值
int oldThr = threshold;
// newCap:扩容之后table数组的大小
// newThr:下次再次触发扩容的条件
int newCap, newThr = 0;
// oldCap > 0 说明条件成立,表示hashMap散列表已经初始化过,是正常扩容
if (oldCap > 0) {
// 扩容之前的table数组已经大小达到最大容量后,则不扩容,且设置扩容条件为int最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容之前的table数组没有达到最大扩容,那就扩容翻倍,如何扩容翻倍呢?用位运算,让oldCap左移一位就实现了翻倍(newCap = oldCap << 1)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// oldCap = 0 等价于 else if (oldCap == 0 && oldThr > 0)
// 说明hashMap中的散列表是null
// 需要新建一个HashMap 有四种构造方法
// 1.new HashMap(initCap,loadFactor);
// 2.new HashMap(initCap);
// 3.new HashMap(map);
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 4.new HashMap();
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//情况1:当前桶位只有一个元素,从未发生过碰撞。
//直接计算出当前元素应存放在 新数组 中的位置,然后扔进去
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//情况2:当前结点已经树化,转红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//情况3:桶位已经形成链表
//链表根据 0 1111与1 1111的最高位进行拆分
//低位链表:存放在扩容之后的数组下标位置,与当前数组的下标一直
Node<K,V> loHead = null, loTail = null;
//高位链表:存放在扩容之后的数组的下标位置为当前数组下标位置+扩容之前数组的长度
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
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;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5.HashMap的get方法分析
根据key获取到对应的value,
第一步,先调用hash(key)得到hashcode
第二步,首先要求(数组不为空)而且(根据寻址算法计算出的index对应的桶里面的元素也不为空)才能往下执行:
①如果根据index定位的桶里面的第一个元素的key与你要查找的元素的key完全一致,那就直接返回
②如果根据index定位的桶里面的元素只有一个,问题是还和你要查找的元素的key不一样,那就直接返回null
③如果根据index定位的桶里面的元素是红黑树分布,另说
④如果根据index定位的桶里面的元素是链表结构,那就一个一个找,如果没找到就返回空
// 获得一个方法
public V get(Object key) {
Node<K,V> e;
// 先调用 hash(key)计算hash值,然后调用 getNode方法
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// getNode方法
final Node<K,V> getNode(int hash, Object key) {
// tab:引用当前hashMap的散列表
// first:桶位中的头元素
// e:临时node元素
// n:table数组元素
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 首先判断 table 不是空且长度不为0,并且first部位null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 对第一个first进行判断,如果hash值相等并且 key 相等,返回当前节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果first的下一个不为null
if ((e = first.next) != null) {
// 如果是树,就调用树的查找方法
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果是链表,就循环进行判断
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 都没有,就返回null
return null;
}
6.HashMap的remove方法分析
根据key移除key-value
第一步,先调用hash(key)得到hashcode
第二步,首先要求(数组不为空)而且(根据寻址算法计算出的index对应的桶里面的元素也不为空)才能往下执行:
①如果根据index定位的桶里面的第一个元素的key与你要查找的元素的key完全一致,那就走删除的逻辑。
②如果根据index定位的桶里面的元素是红黑树分布,那就走红黑树的查找方式,如果找到了就走删除的逻辑。
③如果根据index定位的桶里面的元素是链表结构,那就一个一个找,如果没找到就返回空,如果找到了就走删除的逻辑。
第三步,删除:
①如果传过来的元素是树的结构,那就走树的删除逻辑;
②如果传过来的要删除的元素是桶里面的第一个元素,由于桶里面可能只有一个元素也可能是链表结构,桶里面只有一个元素时下一个元素就是null,桶里面是链表结构时下一个元素就有值,不管下一个元素是null还是有值,反正让tab[index]指向它就行了。
③以上两种情况都不是,那就说明你要删除的是链表中的某个元素,node前面的元素是node0,你干掉了node元素,那就让node0.next=node.next
根据(key,value)移除key-value
很简单,一切都如常,只是删除元素时判断要删除的value和链表中的value是否一致
// 移除元素的方法
public V remove(Object key) {
Node<K,V> e;
// 调用hash方法,获得哈希值,然后调用removeNode
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// 核心方法 removeNode
// hash:hash值
// key:key
// value:value 如果matchValue则匹配的值,否则忽略
// matchValue:如果为true,则仅在值相等时删除
// movable:如果删除虚假不动其他节点
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 是否为空,是否长度为0,且对应的hash值在数组里面存在,才继续向下走
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;
// 判断头元素是不是要删除的元素,如果是就放进去node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 桶的第一个不是
else if ((e = p.next) != null) {
// 树化结构
if (p instanceof TreeNode)
// 调用树化的结果
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
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);
}
}
// 判断是否得到了目标要删除的节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是树节点,调用树的删除操作
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果node = p 表示是第一个数据
else if (node == p)
// 更新地址为下一个数据,放到桶
tab[index] = node.next;
else
// 如果node 不等于 p 那就直接指向链表的下一个元素地址
p.next = node.next;
// 修改次数增加
++modCount;
// 大小减1
--size;
afterNodeRemoval(node);
// 返回删除的node
return node;
}
}
// 如果都没有执行,那么就返回null
return null;
}
6.HashMap的replace方法分析
根据(key,value)替换:
调用get()方法根据key获取到对应的value,如果对应的值不为null,那就替换掉,而且把老值返回出去;
根据(key,Oldvalue,NewValue)替换:
调用get()方法根据key获取到对应的value,如果对应的值不为null而且你传进来的Oldvalue和找到的value一致的话,那就替换掉,替换成功返回true,替换失败返回false;
// 根据 k 和 v 替换
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
// 根据 k oldValue newValue 替换
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
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;
}