(注意:本文基于JDK1.8)
前言
下方截图为HashMap删除元素的所有办法,包括:
1、删除单个元素的方法
2、清空所有元素的方法
清空HashMap的方法中,也包括迭代器中提供的clear()方法
传入一个Object对象的remove()方法分析
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
用于删除单个元素的方法,判断传入的Key对象,匹配Key对象的key-value元素将会被删除,我们分析一下它的方法体
1、定义局部变量存储待删除元素对应的Node对象
首先定义一个局部变量e用于存储removeNode()方法的返回值,Node对象持有了Key对象和Value对象,一组key-value表示为HashMap中的一个元素,添加的元素是Value对象,Key对象只是用于定位Value对象的索引
2、静态方法hash()计算Key对象对应的哈希值
接着最先调用静态方法hash(),并向内传入一个Key对象,静态方法hash()的方法体中,会根据Key对象的hashCode()方法的返回值,再计算出一个新的哈希值
3、调用removeNode()方法
将静态方法hash()的返回值、以及方法中传入的Key对象、一个null值、一个false值、一个true值全部传入到removeNode()方法中,传入的5个参数都表示的是什么呢?我们后面解答
4、将removeNode()方法的返回值赋值给局部变量e存储
removeNode()方法执行结束后,可能会返回一个Node对象或者一个null值,该值会赋值给局部变量e
5、检查局部变量e存储的值,并返回结果
判断局部变量e持有的值是什么,分两种情况返回结果
当局部变量e == null时,整个remove()方法会返回null,说明根据传入的Key对象并没有在HashMap中查找到匹配的元素,返回值null说明删除元素失败……
当局部变量e指向的是一个Node对象时,说明根据传入的Key对象查找到了对应的元素,此时整个remove()方法将返回当前Node对象e持有的value对象,表示从HashMap容器中删除元素成功
重温静态方法hash()
静态方法hash()已经在【第二篇:添加元素】文章中进行了分析,这里再次复习这个静态方法hash()。
疑问:为什么HashMap没有直接使用Key对象的hashCode()方法的返回值,而是再去计算出一个新的值呢?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1、定义局部变量h,用于存储Key对象的hashCode()方法的返回值
局部变量h负责存储Key对象的hashCode()方法的返回值
2、对Key对象本身进行判断,对两种情况分别作出处理
当Key对象为null时,静态方法hash()将会直接返回一个0值
当Key对象不为null时,执行计算新值的逻辑
(1)首先调用Key对象的hashCode()方法获得一个对象本身的哈希值,由局部变量h负责存储
(2)然后再计算出一个Key对象的hashCode()方法的返回值再右移16位后的值
(3)将(1)(2)两个步骤计算出的值,再去进行一个二进制的异或运算
(4)将异或计算后的值作为静态方法hash()的返回值
说明1:静态方法hash()产生的值不是哈希地址,因为只有能直接从数组中定位到元素的值才是哈希地址,真正哈希地址的计算过程:由静态方法hash()的返回值与数组长度-1后的值共同进行按位与运算后的值才是真正的哈希地址(也称桶地址)
说明2:一个Key对象的hashCode()方法的返回值,在Key对象第一次创建后,hashCode()方法的返回值不能再去改变。假设某个Key对象,它的hashCode()方法返回值会跟随着对象的实例变量值的改变而去改变,如果它作为Key对象,会影响HashMap根据同一个Key对象去查找存储的Value对象,所以作为Key对象的要求是:hashCode()的返回值,在对象首次创建后,不能再随意改变!
说明3:不可变对象与可变对象都可以作为Key对象。使用不可变对象作为Key对象,hashCode()的返回值不会改变,如果使用可变对象作为Key对象,只要该Key对象的hashCode()返回值不随着对象的状态改变而去改变即可!
为什么一定要使用静态方法hash()再计算出一个新的值?而不直接使用Key的hashCode()方法的返回值呢?
在静态方法hash()中,其中一个计算过程会将Key对象的hashCode()方法的返回值右移16位,此时Key对象的hashCode()返回值的高位地址会全部移到低位,这样高位地址与低位地址可以一起进行一个异或计算(异或规则:相同为0,不同为1),根本目的:减少哈希碰撞。
12345678123456780000000000000000
^
00000000000000001234567812345678
以下为Peter Lawley的一篇专栏文章《An introduction to optimising a hashing strategy》里的一个哈希碰撞实验:他随机选取了352个字符串,在散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标。
结果显示,当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,没有使用扰动方法【静态方法hash()称为扰动方法】的情况下,HashMap发生103次碰撞,接近30%的碰撞概率。而使用了扰动方法【静态方法hash()称为扰动方法】后只有92次碰撞。碰撞减少将近10%。看来扰动方法(扰动函数)减少了哈希碰撞的概率,JDK1.8中认为扰动二次即可(一次是右移、一次是异或)
如果不使用静态方法hash()进行扰动,则每次都是低位参加数组长度位或的计算,哈希碰撞的概率则相对较高!
对Key对象的hashCode()方法返回值做进一步混淆(扰动),增加“随机度”,可以减少HashMap插入元素时的哈希冲突,这个静态方法hash()的代码也被称作“扰动函数”(扰动方法),JDK1.8中简化了为2次扰动,一次是16位右移,另一次是异或运算。而在JDK1.7中,则是进行了4次扰动!如果直接使用Key对象的hashCode()方法的返回值可以吗?可以是可以,只不过哈希碰撞变动了!(哈希碰撞:不同的Key对象,当他们的hashCode()方法返回值一样时……,称为哈希碰撞)
传入五个参数的removeNode()方法
接受一个int值hash,一个Object对象key,一个Object对象value,标志位matchValue,标志位movable
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
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);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
先介绍一下几个局部变量的作用
tab:tab用于临时持有当前HashMap对象持有的底层数组对象
p:作为桶中的第一个元素时,它代表当前元素对象,其它情况下它代表上一个元素对象
n:代表底层数组长度
index:代表桶的下标
node:表示符合匹配的Node
e:代表下一个Node
k:代表Key对象
v:代表value对象
第一步:首先经过计算,发现Key对象对应(n-1 & hash)的桶处有元素(hash值是一个32位整型值,太大了,数组放不下,所以要用取模运算,使用按位与计算去代替取模运算,所以数组的长度必须是2的n次方,才能保证数组长度减去1后的结果是1111,此时按位与的计算与取模运算的结果一致,但是效率更高)
第二步:尝试找到对应的元素,分为三种情况,1、桶内的第一个元素就是匹配的元素 2、第一个元素不是匹配的元素,判断桶的结构:红黑树或单链表
第三步:对匹配的元素进行移除,也是分三种情况,桶内第一个元素的移除,红黑树结点的移除或者单链表结点的移除
第四步:最后是更新modCount、更新size值,还有一个空的实现方法afterNodeRemoval,是给LinkedHashMap用的
第五步:成功移除结点,会返回一个Node对象,否则最后返回一个null
removeNode()方法的详细拆解分析
1、根据传入hash值、Key对象计算出桶下标、然后检查桶中是否存在元素
Node<K,V>[] tab; Node<K,V> p; int n, index;
先定义四个局部变量
tab代表当前HashMap对象持有的底层数组对象,它的元素类型是Node
p代表桶中的第一个元素、当遍历单链表时,它代表上一个元素对象
n代表HashMap对象持有的底层数组对象的长度
index代表桶的下标
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
……………………省略很多代码………………
}
首先将HashMap对象持有的数组对象table赋值给局部变量tab,然后判断数组对象是否已经创建,若已经创建则代码会继续执行,否则直接会导致整个removeNode方法返回null,那么什么时候会出现此场景?当创建了HashMap对象后,直接去调用remove方法,此时HashMap中持有的数组对象还没有创建,另外也确实没有一个元素
tab不为空的时候,代表数组对象已经创建并被HashMap对象持有,接着取出tab的长度并赋值给局部变量n,然后对数组对象的长度再进行判断,若数组长度为0,方法结束并也会导致removeNode方法返回一个null,那什么时候出现该场景?当创建HashMap对象时,传入的默认容量为0时,再去执行remove方法就会有该现象出现
当tab即不为null、长度也大于0,执行桶计算的逻辑,即(n - 1) & hash,结果会赋值给变量index保存,桶地址再次传入到数组tab中,取出的Node对象会赋值给p,若此此时取出来的桶地址处没有缓存的Node对象,整个removeNode方法结束,并返回null
b、步骤a只是确定桶中有元素,这步是检查桶中的第一个元素是否匹配
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
先定义局部变量四个局部变量,node、e、k、v,将目前桶中的第一个元素p进行查看
先检查元素p的持有的hash值与传入的hash是否相等,相等则会继续判断,不等则会结束判断整个判断过程
若p.hash与传入的hash相等,则先将p持有的key对象赋值给变量k,接着用变量k与传入的key对象进行==的对比
若变量k持有的Node与传入的key对象并不是同一个对象(==为false),会进行下一个判断
传入的key对象必须不能是null,才会进行该判断,即调用传入的key对象的equals方法,传给equals方法的正是第一个元素p持有的key对象
在第一个元素p持有的hash变量值与传入的hash变量值相同的情况下,当传入的key对象不是null时,只要桶中的第一个元素p持有的key对象与传入的key对象(==或equals)有一项相等就代表找到符合要求的元素,此时就会将桶中第一个元素p赋值给临时变量node
c、步骤b是在桶中的第一个元素做匹配,当第一个元素不是匹配的元素时,则证明桶中现在有可能是个单链表或者是个红黑树或者代表桶内就只有一个元素,这里会再进去找匹配的元素
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);
}
}
尝试从桶中第一个元素p中得到第二个元素并赋值给变量e,此刻判断e是否为空,为空则说明桶内就只有一个元素……该代码块阵亡
存储第二个元素的变量e如果不为空,则判断第一个元素p指向的对象是否为TreeNode的对象或子类对象,若是则证明桶内是红黑树结构,接着就将第一个元素p的静态类型Node,向下转型为实际类型TreeNode,目的是为了执行TreeNode下的getTreeNode方法,getTreeNode方法接受方法外侧传入的hash变量值、传入的key对象,这就最终进入红黑树里面去找匹配的元素,最终的结果可能找到也可能没找到,不过都会赋值给node变量(这里为何要通过p去调用红黑树的方法?)
当桶内不是红黑树结构的时候,就会走到单链表结构中,在单链表中,将直接从第二个元素e开始进行查找,仍是先匹配hash、hash相同再对key进行==或equals的判断,只要找到匹配的元素,循环就停止了,而node变量始终存储的成功匹配的元素对象,而在单链表遍历过程中,还巧妙的用变量p临时帮忙保存上一次遍历的元素,用变量p的目的是为了一旦找到匹配的元素,在步骤c中就要让p指向匹配的元素的下一个元素,这是单链表的绝佳删除方式,牛逼…………
d、对匹配到的元素进行处理,匹配的元素一定是存储在node变量中,最后再更新每个HashMap对象持有的配置信息
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);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
根据桶内三种可能出现的情况,都做了不同移除实现:红黑树移除元素、桶内第一个元素移除元素、单链表移除元素
移除的Node对象,会等待被GC回收,此时代表Node对象已经删除,当Java栈中的栈帧出栈,CG ROOTS不存在时,移除的Node会被回收
最后更新modCount值+1(无论是增加减少操作都是+1)
更新HashMap中元素总数
回调一个空实现方法afterNodeRemoval(这个方法是给LinkedHashMap留的方法)
接着就是返回被删除的Node对象
e、这是没有找到匹配元素的执行结果
return null;
它会给调用方法返回一个null
clear()方法分析
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
用于清空HashMap对象持有的所有元素的方法
1、先定义了一个局部变量tab,用于存储持有的数组对象table
2、接着modCount执行++操作,这是为了标记并发读写时实现的fail-fast机制
3、HashMap存储着元素,开始做清空操作
为局部变量tab赋值,若数组对象已经创建、持有的元素总数也大于0,说明HashMap存储着元素
4、先将size变量赋值为0,代表HashMap存储的元素为0个
5、遍历底层数组,全部赋值为null,持有的元素对象交给GC处理
遍历底层数组,把数组中每一个桶都赋值为null,无论数组对象中桶内持有的是单个元素、还是单链表、还是红黑树,全部交给GC去回收对象占用的空间
总结
1、删除元素与添加元素时使用的是同一个静态方法hash()