上次我们已经剖析了put()方法,这次来看看get()方法。
1.HashMap的get()方法剖析:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
可见,也是将key值进行hash()之后,找到对应的桶数组,再调用getNode()进行查找。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//若哈希表不为空,并且当前索引位置有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//若在一个桶中,并且当前索引位置的key值与要查找的key值相等
//说明找到了,返回该值
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//若key值不相同,且还有元素
if ((e = first.next) != null) {
//如果此时已经树化,则调用红黑树的getTreeNode()查找
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;
}
2.get()方法流程总结:
- 首先判断桶数组是否为空,若为空直接返回null。
- 若桶数组不为空,则计算索引,查看当前索引是否有元素,若有元素,查看key值是否相同。若key值相同,则返回该节点。
- 若key值不相同,则继续查找,先判断是否树化,若已经树化,则调用红黑树的getTreeNode()进行查找。
- 若没有树化,则从前往后遍历链表,找到了则返回节点,找不到则返回null。
3.HashMap源码剖析总结:
- 为什么我们不用hashCode()方法计算的出来的hash值作为桶下标呢?
因为hashCode()计算出来的hash值太大,需要大量的存储空间;而且这样哈希表就跟普通的数组差不多。 - 为什么我们要保证哈希表的长度为2^n?
一方面是为了保证哈希表中所有的元素都被访问到,一定程度上避免哈希碰撞;
一方面是为了提高运算速度。
当我们计算元素在哈希表中的索引下标时,采用 i = hash&(n-1),假如我们此时的n为8,则(n-1)=7=0111,就保证了在0-7的范围内,不会发生哈希碰撞,因为此时的i的相与结果完全取决于我们计算出来的hash值。 - 为什么在hash()中计算桶下标要进行(h>>>16)?
这样保留了高16位,使得高低16位都参与异或运算,降低了哈希冲突的概率。
4.树化的条件:
- 当前桶中的链表长度>=8,并且哈希表的长度>=64时,会树化,否则只是进行简单的扩容。
- 加入红黑树是为了提高由于链表过长而造成的查询效率降低,将时间复杂度由O(n)提升为O(logn)。
5.扩容的条件:当桶数组的容量>12时进行扩容。
6.解树化的条件:当红黑树的节点个数<=6时,会将红黑树转为链表。