在 Java 中,HashMap 是一个非常常用的数据结构,用于存储键值对。它提供了快速的查找、插入和删除操作。HashMap 的核心功能之一是根据键获取对应的值,这主要通过 get() 方法来实现。本文将详细介绍 HashMap 的 get() 方法及其相关的辅助方法,包括 getNode()、hash()、getTreeNode()、root()、find()、comparableClassFor() 和 compareComparables()。通过这些方法的解析,您将更深入地理解 HashMap 的内部工作原理。
下面我们先来大致看一下hashMap的get()方法获取数据的大致流程,以便在看下面的源码不至于太迷茫。
HashMap get()方法
//根据key获取Node
public V get(Object key) {
Node<K,V> e;
//计算key的哈希码,调用getNode方法,传入哈希码和键,获取对应的Node
//如果找到的Node为null,返回null;否则返回该Node的值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
下面来看下hash()方法和getNode节点方法
getNode()的流程图比较复杂,我们拆分开来看
getNode()方法获取节点
获取节点大致分为四个步骤:
- 获取桶:根据哈希值计算桶的位置,并获取该桶的第一个节点。
- 直接匹配:检查第一个节点是否匹配,如果匹配直接返回。
- 深入查找:
- 如果是红黑树节点,使用红黑树的方法进行查找。
- 如果是链表节点,遍历链表查找匹配节点。
- 结果返回:找到匹配节点则返回该节点,否则返回 null
//传入哈希码和key
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//检查table是否为空且长度大于0 && 使用(n-1)&hash计算索引桶(n是table长度),并获取该桶的第一个节点first
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//检查first节点是否与给定的hash和key匹配,如果匹配,直接返回first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不匹配,检查first的下一个节点e
if ((e = first.next) != null) {
//如果first是treeNode(即红黑树的根节点),调用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;
}
hash()计算哈希
根据key计算哈希=获取key的哈希码异或上h的右移
//计算哈希
static final int hash(Object key) {
int h;
//如果键为null,则返回哈希码0。HashMap允许使用null作为键,因此需要特殊处理
//如果键不为null,则调用对象的hashCode方法获取哈希码,并将结果赋值给h
//然后对h进行位运算h^(h>>>16)其中>>>是无符号右移操作符,将h的高位右移到低位
//最后将哈希码h和右移后的结果进行异或操作^,得到最终的哈希码并返回
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
getTreeNode()获取树节点
检查当前节点是否有父节点,如果有父节点,说明当前节点不是根节点,调用root()方法获取根据点。
调用find方法在红黑树中查找键值对
//红黑树获取TreeNode
final TreeNode<K,V> getTreeNode(int h, Object k) {
//检查当前节点是否有父节点,如果有父节点,说明当前节点不是根节点,调用root()方法获取根据点。
//调用find方法在红黑树中查找键值对
//find方法是一个递归方法,用于在红黑树查找指定的键值对
return ((parent != null) ? root() : this).find(h, k, null);
}
root方法流程图比较简单啦,一看就懂的那种
root()获取红黑树的根节点
获取当前节点所在的红黑树的根节点
final TreeNode<K,V> root() {
//使用无限循环来遍历父节点,直到找到没有父节点的节点(即根节点)
//初始化r为当前节点this,初始化p为null
//每次循环中,将r的父节点赋值给p
for (TreeNode<K,V> r = this, p;;) {
//如果p为null,说明r已经是根节点,返回r
if ((p = r.parent) == null)
return r;
//否则将p赋值给r,继续循环
r = p;
}
}
find()查找方法
find方法大致逻辑如下:
- 初始化:将当前节点 p 设置为 this。
- 遍历树:
- 获取当前节点的左右子节点。
- 根据哈希值和键进行比较,决定向左或向右子树搜索。
- 如果当前节点的键与目标键匹配,返回当前节点。
- 如果左右子节点之一为空,选择另一个子节点进行搜索。
- 如果键实现了 Comparable 接口,根据比较结果决定搜索方向。
- 递归地在右子树中查找,如果找到匹配节点,返回该节点。
- 否则,继续向左子树搜索。
- 未找到时返回:如果遍历完整棵树后仍未找到匹配节点,返回 null。
//h要查找的键的哈希码;k要查找的键的对象;kc键的可比较类(Comparable类型),用于优化比较过程
//用于在红黑树中查找指定哈希码和键的节点
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
//初始化p为当前节点this
TreeNode<K,V> p = this;
//do-while循环遍历树,直到找到匹配的节点或遍历结束
do {
int ph, dir; K pk;
//获取当前节点p的左节点pl和右子节点pr
TreeNode<K,V> pl = p.left, pr = p.right, q;
//比较当前节点的哈希码ph和目标哈希码h
//如果ph大于h,向左子树搜索(p=pl)
if ((ph = p.hash) > h)
p = pl;
//如果ph小于h,向右子树搜索(p=pr)
else if (ph < h)
p = pr;
//如果ph等于h,进一步比较
//如果键相等(pk==k或k.equals(pk))返回当前节点p
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果左子节点为空,向右子树搜索(p=pr)
else if (pl == null)
p = pr;
//如果右子节点为空,向左子树搜索(p=pl)
else if (pr == null)
p = pl;
//如果键实现了Comparable接口(通过comparableClassFor方法检查)
//使用compareComparables方法比较键,决定向左还是向右搜搜
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
//如果上述条件都不满足,递归地在右子树中查找(p=pr.find(h,k,kc),如果找到返回q
else if ((q = pr.find(h, k, kc)) != null)
return q;
//否则向左子树搜索(p=pl)
else
p = pl;
} while (p != null);
//如果遍历结束后没有找到匹配的节点,返回null
return null;
}
comparableClassFor()方法
目的:确定对象 x 是否实现了 Comparable 接口,并返回其具体的类型。
步骤:
- 检查 x 是否是 Comparable 的实例。
- 如果 x 是 String 类型,直接返回 String.class。
- 获取 x 的类及其泛型接口。
- 遍历泛型接口,检查是否是 Comparable 接口的具体实现。
- 如果找到匹配的类型,返回该类型;否则返回 null。
//用于确定一个对象x是否实现了Comparable接口,并且如果实现了,返回其具体的类型
static Class<?> comparableClassFor(Object x) {
//检查x是否是Comparable的实例
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
//如果x是String类型,直接返回String.class,因为String实现了Comparable<String>
if ((c = x.getClass()) == String.class) // bypass checks
return c;
//获取x的类c及其泛型接口ts
if ((ts = c.getGenericInterfaces()) != null) {
//遍历ts数组,检查每个接口是否是ParameterizedType类型
for (int i = 0; i < ts.length; ++i) {
//如果接口的原始类型(getRawType)是Comparable,并且其实际类型参数(getActualTypeArguments)是一个长度为1的数组
//且这个参数就是c,则返回c
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
//如果没有找到匹配的类型,返回null
return null;
}
compareComparables()
比较两个对象 k 和 x,前提是 k 实现了 Comparable 接口,并且 x 的类型与 k 的类型相同。
步骤:
- 检查 x 是否为空或类型不匹配,如果是,返回 0。
- 将 k 强制转换为 Comparable 类型,并使用 compareTo 方法与 x 进行比较,返回比较结果。
//比较两个对象k和x
static int compareComparables(Class<?> kc, Object k, Object x) {
//如果x为null或者x的类不是kc,返回0
//否则,将k强制转换为Comparable类型,并调用compareTo方法与x进行比较,返回比较结果
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
通过本文的详细解析,我们了解了 HashMap 的 get() 方法及其相关辅助方法的工作原理。get() 方法通过计算哈希码和调用 getNode() 方法来查找指定键的节点。getNode() 方法根据哈希值定位桶,并通过直接匹配或深入查找(链表或红黑树)来获取节点。hash() 方法提高了哈希分布的均匀性。getTreeNode() 和 find() 方法处理红黑树中的节点查找。comparableClassFor() 和 compareComparables() 方法则用于优化键的比较过程。
掌握这些方法的实现细节,可以帮助开发者更好地理解和使用 HashMap,并在实际开发中做出更高效的设计和优化。
更多内容请关注以下公众号