本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
上篇文章JDK8 HashMap源码解析—P1介绍了JDK8
中HashMap
的成员属性、构造函数、put
和resize
方法,今天接着介绍HashMap
的其他方法。
常用方法
get方法
public V get(Object key) {
Node<K,V> e;
//调用getNode方法
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;
//判断索引位置处是否有值 否则直接return null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// hash值和key值都相等 则表明命中 直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//判断是链表还是红黑树
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//是红黑树的话,调用getTreeNode方法查找
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);
}
}
return null;
}
get
方法的流程如下:
-
根据元素的
hash
值和hash(key)
方法定位到数组中索引所在位置; -
判断该索引位置是否存在值:
-
不存在的话,直接返回
null
; -
比较
hash
值和key
,若是相等,则表明命中,返回该索引位置所在的元素; -
若不相等,判断此处是否存在红黑树或者链表结构:
- 若是红黑树,则调用
getTreeNode
方法进行查找; - 若是链表,则遍历链表进行查找;
- 若是红黑树,则调用
-
流程图如下所示:
JDK8中HashMap新增的getOrDefault方法
JDK8
中新增一个getOrDefault
方法,该方法若是根据key
查找不到对应的值value
则返回传入的默认值。源码如下:
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
//可以看出与 get方法的不同 若是查找不到 默认返回defaultValue 其余与get方法一致
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
remove方法
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
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);
}
}
//node不为null 说明命中了要删除的节点
// 如果不需要对比value 或者是需要对比value 但value也相等 则开始进行删除
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);
//如果是首节点的话 直接指向下一个节点 一个的话可以看做是只有一个节点的链表 这样也可以置
//为null
else if (node == p)
tab[index] = node.next;
else
//否则的话,进行链表的移动
p.next = node.next;
//更新modCount
++modCount;
//更新size
--size;
afterNodeRemoval(node);
//返回删除的节点
return node;
}
}
return null;
}
remove
方法流程梳理如下:
-
利用元素的
hash
和hash(key)
方法进行定位 -
若是当前索引位置处为
null
,直接返回null
; -
比较
hash
和key
,若相等,则表明找到了待删除的节点 -
若不是,则判断该位置是红黑树还是链表:
- 若是红黑树,则调用
getTreeNode
方法进行查找; - 若是链表,则遍历链表进行查找;
- 若是红黑树,则调用
-
若是找到了待删除节点,则开始进行删除:
- 若该索引位置处为红黑树,则调用
removeTreeNode
方法进行删除; - 若是链表,则执行链表相关的操作进行删除;
- 若该索引位置处为红黑树,则调用
-
最后更新
modCount
和size
等属性的值;
扩展
为什么数组长度都是2的倍数?
- 当数组都是2的倍数时,
2^n-1
的二进制表示中所有位置都是1,这样与一个全部都是1的二进制数进行 & 操作时,速度会大大提升; - 计算元素的索引位置时,一般采用的是
%
操作,但是如果数组长度都是2的倍数的话,hash & (length-1)
等价于hash % length
,但是&
操作的效率更高,因为%
在操作系统会进行转换,&
操作不用; - 数组长度为2的倍数时,不同
key
计算出相同的index
的概率较小,减少hash
碰撞;
为了减少hash碰撞,hashMap做了哪些操作?
hash
方法中,hashCode ^ hashCode >>>16
,这样所得的hash
值可以将hashCode
的高位和低位都利用上,降低不同key
通过hash
方法获得相同hash
值的概率,减少hash
冲突;- 计算索引位置时,
hash & (length-1)
,由于length
始终是2的倍数,length-1
的二进制表示中各位都是1,一个数与各个位都是1的数进行&
操作,进一步降低hash
冲突;
总结
这里详细介绍了JDK8
中HashMap
的get
方法和remove
方法,可以看到JDK8
中的get
和remove
方法因为增加了红黑树这个数据结构,所以在查找的时候要考虑节点为红黑树的情况。最后补充了两个扩展知识,从这个知识点就能看出HashMap
设计的精妙之处。总之,HashMap
这个数据结构对Java
程序员来说是必须掌握的知识点,希望大家结合上篇文章JDK8 HashMap源码解析—P1一起消化下,争取化为己用。(PS:再说一遍,写出HashMap
的程序员们真的🐮)