Java集合之 HashMap底层实现原理(JDK1.8)
2.1 HashMap
HashMap是Java中最常用K-V容器,采用了哈希的方式进行实现,HashMap中存储的是一个又一个Key-Value的键值对,我们将其称作Entry,HashMap对Entry进行了扩展(称作Node),使其成为链表或者树的结构存储在HashMap的容器里(是一个数组)。
特点:
(1)key可以为空(null),由于键不可重复,因此只能有一个为null, value可以为空(null)。
(2)HashMap中的数据是无序的,键不可重复的。
(3)非线程安全的,效率高。
2.1.1 HashMap底层实现原理
JDK1.8中hashMap的组成
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//默认初始容量,是HashMap的最小容量为16,容量就是数组的大小也就是变量,transient Node<K,V>[] table。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量,该数组最大值为2^31次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子,如果构造的时候不传则为0.75,用于扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//一个位置里存放的节点转化成树的阈值,也就是8,比如数组里有一个node,这个node链表的长度达到8才会转化为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当一个反树化的阈值,当这个node长度减少到该值就会从树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//满足节点变成树的另一个条件,就是存放node的数组长度要达到64
static final int MIN_TREEIFY_CAPACITY = 64;
//具体存放数据的数组
transient Node<K,V>[] table;
//entrySet,一个存放k-v缓冲区
transient Set<Map.Entry<K,V>> entrySet;
//size是指hashMap中存放了多少个键值对
transient int size;
//对map的修改次数
transient int modCount;
//阈值 表示当前HashMap能够承受的最多的键值对数量,一旦超过这个数量HashMap就会进行扩容
int threshold;
//加载因子
final float loadFactor;
}
从以上源码中可以看出hashMap的组成是什么样的,由于文中所有源码的分析均基于JDK1.8,因此,在JDK 1.8中,HashMap的底层数据结构是“数组+链表+红黑树”,即在链表的长度超过阈值8时转化为红黑树结构。而在1.8之前,HashMap的底层数据结构只有“数组+链表”。这样做的原因是什么?接下来具体分析一下HashMap的底层实现。
首先,我们先回顾一下数组和链表。
数组的特点:它的存储区间是连续的,占用内存严重,空间复杂也很大,时间复杂为O(1)。
优点:是随机读取效率很高,因为数组是连续的(随机访问性强,查找速度快)。
缺点:插入和删除数据效率低,由于插入数据的时候插入位置后面的数据在内存中要往后移动,且大小固定不易动态扩展。链表的特点:区间离散,占用内存宽松,空间复杂度小,时间复杂度O(N)。
优点:插入删除速度快,内存利用率高,没有大小固定,扩展灵活。
缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)。
那么,有没有一种数据结构,查询效率高并且插入删除的效率也高呢?哈希表就是这样一种数据结构,哈希表是基于哈希函数建立的一种查找表,hash函数就是根据key计算出应该存储地址的位置(地址index=H(key)),它其实就是将数组和链表两种数据结构相结合,简单的说就是每个元素都是链表的数组。在JDK1.8之前,HashMap由数组和链表来实现对数据的存储,也就是哈希表,使用链表处理hash冲突,具体的在源码中,HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。下图表示了JDK1.7的HashMap数据结构。
在JDK1.8中,对HashMap的底层实现进行了优化,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树,当长度降到6就转成普通bin。在性能上进一步得到提升。下图表示了JDK1.8的HashMap数据结构。
接下来分析一下JDK1.8的源码中涉及到的数据结构,也解释一下为什么链表长度为8时要转换为红黑树的问题?
在HashMap的源码注释中其实已经说明其实现结构。
/*
* Implementation notes.
*
* This map usually acts as a binned (bucketed) hash table, but
* when bins get too large, they are transformed into bins of
* TreeNodes, each structured similarly to those in
* java.util.TreeMap.
* 说是map通常当做binned(存储桶)的哈希表,但是当bin太大时,它们将转换为TreeNodes的bin,每个bin的结构与java.util.TreeMap中的相似。
(1)数组
//具体存放数据的数组
transient Node<K,V>[] table;
(2)链表
//数组元素Node<K,V>实现了Entry接口,Node是单向链表,它实现了Map.Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//构造函数(Hash值, 键, 值, 下一个节点)
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断两个node是否相等,若key和value都相等,返回true。
public final boolean equals(Object o) {
if (o == this)//可以与自身比较为true
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Map接口中有一个Entry接口,在HashMap中对其进行了实现,Entry的实现是HashMap存放的数据的类型。其中Entry在HashMap的实现是Node,Node是一个单链表的结构,TreeNode是其子类,是一个红黑树的类型。
(3)红黑树
/**
* Entry for Tree bins.(bin是指的table中某一个位置的node,一个node可以理解成一批数据。)
* Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links 父节点
TreeNode<K,V> left; //左子树
TreeNode<K,V> right;//右子树
TreeNode<K,V> prev;// needed to unlink next upon deletion
boolean red; //颜色属性
//构造函数包含hash,键,值和下一个节点
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.返回当前节点的根节点
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
我们都知道,链表的时间复杂度是O(n),红黑树的时间复杂度O(logn),很显然,红黑树的复杂度是优于链表的,既然这样,那为什么hashmap不直接使用红黑树呢?
从时间复杂度来分析,红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
/*Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
从具体源码中来分析:源码中的注释写的很清楚,因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树,说白了就是trade-off,空间和时间的权衡。那为什么达到阈值8才会选择使用红黑树呢?当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,而且根据统计,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以选择阈值是8,是根据概率统计决定的。而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树来提高性能。也就是说在大部分情况下,hashmap还是使用的链表,如果是理想的均匀分布,节点数不到8,hashmap就会自动扩容,看下面链表转为红黑树的方法的源码就可以:
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* 除非hash表太小(在这种情况下将调整大小),否则将替换给定哈希值的索引处bin中所有链接的节点。
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
在该方法中,有这样一个判断,数组长度小于MIN_TREEIFY_CAPACITY,就会扩容,而不是直接转变为红黑树,因此可不是什么链表长度为8就变为红黑树,还有别的条件。
//满足节点变成树的另一个条件,就是存放node的数组长度要达到64
static final int MIN_TREEIFY_CAPACITY = 64;
因此在通常情况下,链表长度很难达到8,但是特殊情况下链表长度为8,哈希表容量又很大,造成链表性能很差的时候,就会采用红黑树提高性能,这就是为什么链表长度为8时要转换为红黑树的原因。那我们红黑树退化为链表的情况又是怎么样的呢?下面将介绍HashMap中的扩容问题。
2.1.2 HashMap构造函数
//只带有初始容量的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//无参构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//用m的元素初始化散列映射
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
//带有初始容量和加载因子的有参构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) //容量不能为负数
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//当容量大于2^31就取最大值1<<31;
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//加载因子为正
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//当前数组table的大小,一定是2的幂次方
//tableSizeFor方法保证了数组一定是是2的幂次方,是大于initialCapacity最接近的值
this.threshold = tableSizeFor(initialCapacity);//新的阈值
}
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
该方法将一个二进制数第一位1后边的数字全部变成1,然后再加1,这样这个二进制数就一定是100…这样的形式。此处实现在ArrayDeque的实现中也用到了类似的方法来保证数组长度一定是2的幂次方。返回距离指定参数最近的2的整数次幂,例如7->8, 8->8, 9->16等。
2.1.3 HashMap扩容机制resize()
先说下前面多次提到的负载因子:源码中有个公式为threshold = loadFactor * 容量。HashMap和HashSet都允许你指定负载因子的构造器,表示当负载情况达到负载因子水平的时候,容器会自动扩容,HashMap默认使用的负载因子值为0.75f(当容量达到四分之三进行再散列(扩容))。当负载因子越大的时候能够容纳的键值对就越多但是查找的代价也会越高。所以如果你知道将要在HashMap中存储多少数据,那么你可以创建一个具有恰当大小的初始容量这可以减少扩容时候的开销。但是大多数情况下0.75在时间跟空间代价上达到了平衡所以不建议修改。
resize() 函数会在两种情况下被调用:
(1) HashMap new 出来后还没有 put 元素进去,没有真正分配存储空间被初始化,调用 resize() 函数进行初始化;
(2) 原 table 中的元素个数达到了 capacity * loadFactor 这个上限,需要扩容。此时调用 resize(),new 一个两倍长度的新 Node 数组,进行rehash,并将容器指针(table)指向新数组。
边也可以引申到一个问题HashMap是先插入还是先扩容:HashMap初始化后首次插入数据时,先发生resize扩容再插入数据,之后每当插入的数据个数达到threshold时就会发生resize,此时是先插入数据再resize。
/** 初始化或增加表大小。 如果为空,则根据字段阈值中保持的初始容量目标进行分配.
* 否则,因为我们使用的是2的幂,所以每个bin中的元素必须保持相同的索引,或者在新表中以2的幂偏移。
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//首次初始化后table为Null
int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取原哈希表容量 如果哈希表为空则容量为0 ,否则为原哈希表长度
int oldThr = threshold;//获取原哈希表扩容门槛,默认构造器的情况下为0
int newCap, newThr = 0;//初始化新容量和新扩容门槛为0
//如果原容量大于 0,这个if语句中计算进行扩容后的容量及新的负载门槛
if (oldCap > 0) {
//判断原容量是否大于等于HashMap允许的容量最大值 2的30次幂
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;//满足则把当前HashMap的扩容门槛设置为Integer允许的最大值
return oldTab;//不再扩容直接返回
}
/**
* newCap = oldCap << 1 ; 类似 newCap = oldCap * 2 移位操作更加高效
* 表示把原容量的二进制位向左移动一位,
* 扩大为原来的2倍,同样还是2的n次幂
* 如果新的数组容量<HashMap允许的容量最大值2的30次幂
* 并且原数组容量>=默认的初始化数组容量2的4次幂 =16
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; //新的扩容门槛为原来的扩容门槛的2倍,同样二进制左移操作
}
/**
* 如果原数组容量小于等于零并且原负载门槛大于0,则新数组容量为原负载门槛大小
*/
else if (oldThr > 0)
newCap = oldThr;
/**
* 在默认构造器下进行扩容:初始化默认容量和默认负载门槛
* 如果原数组容量小于等于0并且原负载门槛也小于等于0,则新数组容量为默认初始化容量16
* 新负载门槛为默认负载因子(0.75f) * 16=12;
*/
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/**
* 如果新负载门槛为 0 则开始使用新的数组容量进行计算
*/
if (newThr == 0) {
float ft = (float)newCap * loadFactor;// 新的数组容量 * 负载因子
//如果新数组容量小于HashMap允许的最大容量(2的30次幂)并且新计算的负载门槛小于HashMap允许的最大容量(2的30次幂),则新的负载门槛为计算后的值的最大整型,否则新的负载门槛为Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//设置全局负载门槛为计算后的新的负载门槛
/**
* 根据新的数组容量创建新的哈希桶 赋值给newTab
*/
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;//把新创建的哈希桶赋值给全局table
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1)
// 扩容都是按照2的幂次方扩容,因此newCap = 2^n
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 当前index对应的节点为红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 把当前index对应的链表分成两个链表,减少扩容的迁移量
Node<K,V> loHead = null, loTail = null;//定义两个指针,分别指向低位头部和低位尾部
Node<K,V> hiHead = null, hiTail = null;//定义两个指针,分别指向高位头部和高位尾部
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 扩容后不需要移动的链表
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 扩容后需要移动的链表
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null; // help gc
newTab[j + oldCap] = hiHead; // 扩容长度为当前index位置+旧的容量
}
}
}
}
}
return newTab;
}
2.1.4 HashMap存取原理
(1)HashMap中put()
put()实现原理:第一步:将k,v封装到Node对象当中(节点)。第二步:它的底层会调用K的hashCode()方法得出hash值。第三步:通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//当hash到的位置,该位置为null的时候,存放一个新node放入,p赋值成了table该位置的node值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//该位置第一个就是查找到的值,将p赋给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是红黑树,调用红黑树的putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//是链表,遍历
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//注意e = p.next这个一直将下一节点赋值给e,直到尾部,注意开头是++binCount
p.next = newNode(hash, key, value, null);
//当链表长度大于等于7,插入第8位,树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* Tree version of putVal.
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
(2)HashMap中get()
get()实现原理:第一步:先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。第二步:通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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 &&
//找到要查询的Key在table中的位置,table是存放HashMap中每一个Node的数组
(first = tab[(n - 1) & hash]) != null) {
//Node可能是一个链表或者树,先判断根节点是否是要查询的key
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//有子节点
if ((e = first.next) != null) {
//红黑树查找
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;
}
/**
* Calls find for root node.
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
*/
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;
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;
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);
return null;
}