JDK1.8---HashMap的get(Object)方法源码详解
HashMap是我们常用的集合之一,本片针对HashMap中的get()方法底层源码讲解。
之后还会有对HashMap的put、remove、还有扩容方法resize以及链表扭转为红黑树进行分析
下面针对HashMap从get()方法开始以及涉及到的调用方法讲解:
先看一下get()方法源码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
这个方法没有什么要说的,主要是getNode()方法才是HashMap通过K查找V的灵魂所在,getNode()方法的第一个参数是根据要查找的K获取K的hash值(注意:在通过K查找V时就是通过该K的hash值和HashMap的桶进行了一个&运算定位到该K-V所在的桶后面会讲到),下面来详细看看getNode()方法是如何实现的:
为了详细理解到HashMap,首先看看获取K的hash方法吧:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
该方法是对K的hashCode所得到的值进行一个波动处理,是为了防止一些实现比较差的hashCode方法造成碰撞太多(没有均匀分布在各个桶中)。(为何波动处理之后就可以均匀分布在各个桶中原理我也不了解,希望知道的留言指教
)
接下来看看getNode()方法是如何实现的吧:
1 final Node<K,V> getNode(int hash, Object key) {
2 Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
3 if ((tab = table) != null && (n = tab.length) > 0 &&
4 (first = tab[(n - 1) & hash]) != null) {
5 if (first.hash == hash && // always check first node
6 ((k = first.key) == key || (key != null && key.equals(k))))
7 return first;
8 if ((e = first.next) != null) {
9 if (first instanceof TreeNode)
10 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
11 do {
12 if (e.hash == hash &&
13 ((k = e.key) == key || (key != null && key.equals(k))))
14 return e;
15 } while ((e = e.next) != null);
16 }
17 }
18 return null;
19 }
中所周知HashMap是用过数组+链表实现的,数组就相当于一个个桶,桶中放着链表。
[1] 要想通过K查找V,首先需要定位到该K所在的桶(哪一个数组下表中),也就是在第4行进行定位的,hash & (n - 1) ] n为桶数也就是数组大小,定位的时候我们首先想到的就是hash%n,但是这种取余操作相对于&操作效率太低了,而要使hash&(n-1) == hash%n这两个公式相等有一个前提条件就是n必须为2次幂,这也就解释了HashMap的大小为什么是2的n次幂, 每当定位到K所在的桶就会获取桶中链表的头结点,先判断头结点,如果相同就返回头结点,否则继续向下查找。
这里需要注意的是查找时,先通过该K的hash值与链表中各个结点的K的hash值进行对比,如果不同继续对比下一个结点,如果相同,该通过 equals() 方法进行对比判断是否真的是同一个K,若果相同就返回该结点。
[2] 当第一个头结点对比完了,若果不同就需要对比第二个结点,在这里也有一个需要注意的地方,也是接下来要讲的主要内容,从JDK1.8开始对HashMap的链表进行了一个优化,当链表中Node结点数量大于等于8的时候,会把链表扭转为一棵红黑树,所以在判断第二个结点之前就需要判断该桶中是链表,还是红黑树,在第9行进行判断的,如果是链表就一直向下迭代判断知道该链表结束。
[3] 在步骤[2]中如果桶中存放的是红黑树,就要通过红黑树的方法进行搜索判断,这就是下面的重点,通过红黑树搜索是通过第10行的getTreeNode()方法进行搜索的,首先看下源码:
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
通过红黑树查找,首先要保证从根节点进行查找,这里通过一个三目运算符进行保证的,如果parent==null也就意味着从根节点开始搜索的,如果不为null就要通过root()方法返回根节点,源码如下:
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
当有了根节点也就要开始进行所搜了也就是根节点调用的find()方法,首先看下源码:
1 final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
2 TreeNode<K,V> p = this;
3 do {
4 int ph, dir; K pk;
5 TreeNode<K,V> pl = p.left, pr = p.right, q;
6 if ((ph = p.hash) > h)
7 p = pl;
8 else if (ph < h)
9 p = pr;
10 else if ((pk = p.key) == k || (k != null && k.equals(pk)))
11 return p;
12 else if (pl == null)
13 p = pr;
14 else if (pr == null)
15 p = pl;
16 else if ((kc != null ||
17 (kc = comparableClassFor(k)) != null) &&
18 (dir = compareComparables(kc, k, pk)) != 0)
19 p = (dir < 0) ? pl : pr;
20 else if ((q = pr.find(h, k, kc)) != null)
21 return q;
22 else
23 p = pl;
24 } while (p != null);
25 return null;
26 }
所搜时是通过K的hash值进行搜索步骤如下:
[1] 如果根结点的K的hash值>要查找的K的hash值就走根节点的左子树进行搜索,反之进行右子树搜索;
[2]根据[1]得到的结点,结点的K通过equals方法进行比较K结果相等就返回该结点;
[3] 如果hash值相等但是通过equals比较K的结果不相等就继续向下比较,再比较时先判断该结点下面是否还有左右子树,如果没有了就返回null,也就是HashMap中不存在对应该K的K-V键值对;
(注意以下步骤是在hash值相等但是K不相等的时候进行的)
[4] 如果该节点仅有左子树或者右子树其中之一,则就根据剩余的那个子树走;
[5] 如果左右子树都有,这时候在这里进行了一个方法的递归调用(也就是左右两边都走了一遍),也就是在上面源代码的第20行 ,先通过右子树进行了方法的递归调用,在向下面进行了左子树的搜索。
从根节点到叶子节点循环上面5个过程,如果到叶子结点还没有找到,返回null。
以上是我通过对HashMap的get方法源码的理解,如果那里不对,希望留言批评指正。