前言
上文中介绍了hashmap的常量参数以及构造方法,这次继续来解析其他方法。
正文
先看下源码
// get方法,调用getNode方法
public V get(Object key) {
// 内部类节点e
Node<K,V> e;
// 调用getNode方法,传入key的hash值以及,key,判断返回的e是否为null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
接下来,看下内部类Node
实现了Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 构造参数里hash,k,v不多赘述,next存储的是指向下一个的node的引用地址
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
再看下hash方法
// 计算hash的值是将key的hashcode值赋给h
// h右移16位后,载进行异或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这段代码叫扰动函数,大概解释一下,大家都知道key的hash函数算出的来数一个散列值,散列是int型,散列值的范围是很大的,足足超过了hashmap的长度上限,所以对要数组长度进行取模运算来获取下标。然而虽然散列值范围很大,但实际map长度很低,大多也就十几到上百的长度,这样取模依然很容易造成hash碰撞,即算出的下标都是同一位,这样大多数据就存在同一个下标下的链表,这样造成的后果就是查询效率很低,所以这个扰动函数的存在就是为了降低hash冲撞而进行的。
所以将自己的hashcode右移16位进行异或,即高16位于低16位进行异或运算,尽量减小hash碰撞的可能性。
继续看它的调用方法getNode(hash(key), key)
*/
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) {
// 判断头结点的hash值,然后比较key的值是否与入参相同
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果查询的匹配头结点失败,判断头结点的next指向下一个节点是否存在,即当前数组长度是否大于1.
if ((e = first.next) != null) {
// 判断头节点是否是树节点,是从树中获取
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 不是的话,就循环遍历链表挨个比较直到找到key相等的值。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 如果以上操作都没找到就返回null
return null;
}
然后看下getTreeNode
方法
// 先判断有没有父节点,有去定位到根节点,没有的话即当前节点为根节点
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
// 循环遍历树,从当前入参节点向上遍历找到根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
最后,在看下遍历树的方法find
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
// 左子树,右子树
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 大于即进左子树,小于则右子树
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
// 循环遍历直到找到相等的key
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
// 如果类实现了Comparable接口,才会走进这里,进行compare比较
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
// 如果树遍历完也没有找打,则返回null
return null;
}
简单介绍一下二叉查找树的树结构,二叉查找树只有一个根节点,每个节点的左孩子大于父亲,右孩子小于父亲。
如图就是一个二叉树,而红黑树是一个平衡二叉树,平衡二叉树的性质它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,相对于传统的链表长度遍历所花费的O(n),二叉树则近乎于折半查找O(logn),性能也高很多,但普通二叉树的极端插入会导致单边树,即所有节点都会大于或小于根节点,这样查询效率和链表一样O(n),而红黑树不会出现这样状况。本文不会详情展开关于红黑树的数据结构描写,在后续文章里笔者会专门介绍红黑树的原理。
后话
本文主要介绍了hashmap的hash思想以及get方法,其中原理是先查询头节点,然后比较如擦的hash以及key,失败的话则判断是链表还是树进行递归查询,直到找出结果或者没找到返回null。