前言
HashMap 是java集合基础中非常重要的一个知识点,其原因是HashMap拥有高效的性能,为了成为一个重实战原理的Java工程师,而不是只会调用,必须要深入剖析HashMap其原理。本章将不会按照源码的排列顺序来剖析,而是通过诞生原因开始,以故事的形式逐个引出和解析相关变量和方法。介绍背景在JDK 1.8版本基础上。本章内容均为个人理解,如有错误请指正。
探讨问题
- hashMap的设计思想是什么
- hashMap为什么会设计成key-value形式
- hashMap为什么是无序结构
- hashMap为什么会有加载因子
- hashMap有什么优缺点?
诞生背景
HashMap中最常用的两个方法是get()、和put()。它们代表了hashMap最重要的存取操作,因此hashMap是想被设计成一种方便存取的集合。前面章节分析的存取集合有数组、链表等等。但是在计算机中,数组是随机存取结构中效率最高的一种,通过下标获取元素的时间复杂度为O(1),而链表则为O(n),通过二叉树来获取元素的时间复杂度为O(log n)。因此使用数组作为元素存储结构是最理想的方案,在内存中,数组是一连串单元的集合,只要记住了数组下标,就可以获取到对应的元素。因此下标和值是一对一的关系。但是随着数据增多,下标只是简单的数字,容易混淆,在使用过程中难以记忆。假如能把下标变成能方便记住的符号,就可以直接通过这个符号来获取元素了。
于是,hashMap出现了。
数组诞生
HashMap为了实现成key-value的设计思想,其内部通过维护一个数组,将元素存在数组中,key-value关系通过数组中的(下标—值)来模拟。
table变量
transient Node<K,V>[] table;
table变量,是HashMap内部维护的一个数组,由它来实现数组结构,
HashMap明确定义了数组的初始化长度为16。
/**
* The default initial capacity - MUST be a power of two.
* 翻译: 数组的默认初始化容量,数值必须是2的幂(2^n)。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
在Java中,数组是一种不可变长度的结构,如果要实现扩容,则必须重新创建一个新的数组,有意思的是,上面明确规定了数组的容量必须是2的幂,如4、8、16、32、64…,hashMap在数组扩容上怎么实现的呢?
扩容
tableSizeFor(int cap)方法
/**
* Returns a power of two size for the given target capacity.
* 返回一个2的幂的数值
*/
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;
}
上述是hashMap的计算容量方法,通过传入一个值,来计算应该返回的容量。
如果传入一个2的幂和非2的幂,其返回值是多少呢?
传入8
static final int tableSizeFor(int 8) {
int n = 8 - 1; // 则 n = 7
n |= n >>> 1; // 00000000 00000000 00000000 00000111
// 00000000 00000000 00000000 00000011
// 其或运算得: 00000000 00000000 00000000 00000111
n |= n >>> 2; // 00000000 00000000 00000000 00000111
// 00000000 00000000 00000000 00000001
// 其或运算得: 00000000 00000000 00000000 00000111
n |= n >>> 4; // 00000000 00000000 00000000 00000111
// 00000000 00000000 00000000 00000000
// 其或运算得: 00000000 00000000 00000000 00000111
n |= n >>> 8; // 00000000 00000000 00000000 00000111
// 00000000 00000000 00000000 00000000
// 其或运算得: 00000000 00000000 00000000 00000111
n |= n >>> 16; // 00000000 00000000 00000000 00000111
// 00000000 00000000 00000000 00000000
// 其或运算得: 00000000 00000000 00000000 00000111 == 7
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
返回结果: 7+1 = 8
}
如果传入非2的幂数值呢?
传入9
static final int tableSizeFor(int 9) {
int n = 9 - 1; // 则 n = 8
n |= n >>> 1; // 00000000 00000000 00000000 00001000
// 00000000 00000000 00000000 00000100
// 其或运算得: 00000000 00000000 00000000 00001100
n |= n >>> 2; // 00000000 00000000 00000000 00001100
// 00000000 00000000 00000000 00000011
// 其或运算得: 00000000 00000000 00000000 00001111
n |= n >>> 4; // 00000000 00000000 00000000 00001111
// 00000000 00000000 00000000 00000000
// 其或运算得: 00000000 00000000 00000000 00001111
n |= n >>> 8; // 00000000 00000000 00000000 00001111
// 00000000 00000000 00000000 00000000
// 其或运算得: 00000000 00000000 00000000 00001111
n |= n >>> 16; // 00000000 00000000 00000000 00001111
// 00000000 00000000 00000000 00000000
// 其或运算得: 00000000 00000000 00000000 00001111 == 15
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
返回结果: 15+1 = 16
}
因此,可以通过上述结果得到结论,如果传入2的幂,那么数组容量应该为该值,如果传入非2的幂,则数组容量应该是大于该值并且为离它最近的一个2的幂。这个在初始化hashMap时构造器中经常使用。
构造器
HashMap提供了三类构造器
无参构造器
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
无参构造器的初始化容量为16.
指定容量构造器
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
指定容量的构造器的HashMap最终容量并不是指定的值,而是通过tableSizeFor(int cap)方法计算后的容量值。具体实现看最后一个构造器:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); // 这里根据传入的值,通过tableSizeFor(int cap)计算容量。
}
因此可以得到结论,如果在构造HashMap时传入了一个2的幂(如8,16,32),则数组最终容量也为该值(如8、16、32).如果传入的是非2的幂(如4、5、9、13)等等,则数组最终容量应该为大于该值的最近2的幂(如8、8、16、16)。
现在数组结构有了,要实现key-value形式,那么如何将数组下标转成一个符号呢?或者说将一个符号转换成一个下标?
哈希算法
这里需要引出哈希算法了,hashMap中的数组指定了存储类型为Node对象
transient Node<K,V>[] table;
Node是一个代表一个元素结点,它内部存储了具体的key值和value值,同时维持着一个hash值。
HashMap中提供了一个计算结点hash值的方法,如下。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个hash算法是通过传入的key值来计算的。变量h代表key的原始hashCode值,它是一个int值,也就是说它的hashCode有32位长度,通过无符号右移16位(一半)以后,再与自身进行异或运算。具体的hash算法不是这里的研究重点。反正通过hash算法,将key值计算成一个hash值,维持在每个元素结点中。
这里需要注意的一点就是,hash算法是通过调用入参对象的hashCode方法来计算的,因此hash值的计算依赖于目标对象的hashCode方法,调用的native方法,不同的对象其hashCode都不一样,即使它们内部维持着的对象的hash值是一样的。
实例演示
class Student{
String name;
public Student(String name){
this.name = name;
}
}
在这个Student类中,现在new两个对象。
Student s1 = new Student("张三");
Student s2 = new Student("张三");
两个Student中,它们都维持着相同的 “”张三“,它们具有相同的hash值,按照HashMap中的设计本意,它们的hash值应该是一样的; 但是由于没有重写hashCode,在虚拟机看来,s1 和 s2 是两个对象,它们的hash值是不一样的。因此就会造成HashMap中的hash算法不能按照我们的设计本意来使用。
但是有一个问题就是,哈希算法虽然好用,但是哈希算法会因为某些原因形成哈希冲突,两个不同的key值计算出来是同一个哈希值。这怎么办呢?
链表诞生
解决哈希冲突的方法目前有几种:
(1) 开放定址法
(2)链地址法
(3)再散列法
通过链地址法,将冲突的哈希元素通过链表的方式连接起来,相同的哈希元素通过不断的在链表后新增结点来解决问题。
Node结点
Node结点,是HashMap的内部类,作为一个结点。hashMap的key—value值就是存储在Node结点中的。同时它内部维护着一个表示该结点的hash值和向后的指针next,防止因hash冲突产生的数据覆盖,造成数据丢失,因此通过next可以形成链表,来解决hash冲突。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 结点的hash值
final K key; // hashMap中元素的key值就是存放在这里
V value; // hashMap中key对应的value值就在这里,key-value 的变量关系是一对一。所以一个key值只有一个value值,
// 如果key 或者 value 被设计成数组,那可能是一对多关系了。
Node<K,V> next; // 指向下个结点的指针
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;
}
public final boolean equals(Object o) {
if (o == this)
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;
}
}
HashMap内部Node结点中维护着一个int的hash变量,上面有描述。hash变量采用的是int类型,因此注定hashMap内部容量允许范围内最大只能为2^31,因此关于容量HashMap中有明确定义变量:
static final int MAXIMUM_CAPACITY = 1 << 30; 即 2^30
这个变量规定了HashMap中最大容量不超过2^30个元素结点。
内部结构图
通过上面组成结构的分析,大概知道了HashMap内部通过Node对象数组来实现数组结构,通过Node来实现链表结构。一个Node对象中存放着key-value值,然后Node对象可以存放在数组中,Node结点之间也可以相互链接。如下图:
链表的诞生解决了hash冲突,现在每个元素结点中,都有了一个hash值,那么如何通过hash值来计算数组下标呢?hashMap中的元素为什么是无顺序的呢?上图中的数组为什么有些位置是空的呢?
hash定位
put方法是HashMap最常用最重要的一个方法,由于hashMap内部结构的不确定性,有可能是数组,有可能是数组+链表,也有可能是数组+链表+红黑树。当操作一个put方法时,如何通过hash值来确定数组中的位置,如果出现了哈希碰撞,又如何来处理呢?
这个操作主要是在putVal方法中有一段,
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
.
.
其中 p = tab[i = (n - 1) & hash] 就是通过hash值来定位数组下标的方法,n代表当前数组的容量,那么n-1即为最大下标值,通过key值计算出的结点hash值和最大下标值的与运算中,即使hash值很大,但由于是与运算,所以最大值不会超过n-1.
假设 n=16,n-1即为 00000000 00000000 00000000 00000111
假设 hash为任意数字 01010100 10101010 10101011 10001000
那么与运算过程中,超过n-1的高位部分将全部为0,所以该运算值最大为n-1,最小为0。
这个操作不会导致数组下标越界。但是通过hash值计算出的下标也是没有顺序和规律的。
所以这个就是hashMap中为什么元素是随机存储的,并且不会越界。因为它是根据当前最大容量计算出来的。所以最大容量影响hash碰撞,容量越大,hash碰撞越小。
链表的诞生解决了哈希冲突,诞生链表带来的坏处就是链表索引的时间复杂度为O(n),偏离了O(1)的最初设计思想,所以为了使hashMap尽量变得高效,就要尽量避免链表的产生,也就是减少哈希碰撞的概率。通过hash定位算法可知,数组的容量越大,hash碰撞的概率就会越小,但是由于数组在内存中是连续的存储结构,过大的容量会造成空间浪费,增加空间成本。过小的容量会产生哈希冲突,增加查询时间成本,那么如何在两者之间进行权衡呢?
于是,负载因子产生了。
负载因子
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
系统中默认的负载因子是0.75,是一种权衡空间与时间的设计,负载因子越大,会增加时间成本,减少空间成本。负载因子越小,会增加空间成本,降低时间成本。这个需要开发者根据业务场景来权衡。
但是即使碰撞概率可以减小,但是哈希碰撞依然会存在。在大量数据场景下,碰撞概率不变但是碰撞数量却会持续增加,造成链表越来越长。链表长度的变长增加了hashMap的索引时间,那么如何优化逐渐变长的链表呢?
红黑树诞生
为了解决这个问题,hashMap在JDK1.8版本中引入了红黑树的数据结构,当链表增加到一定长度时,将链表转换为红黑树。这个临界数值HashMap也有明确的定义:
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 翻译: 这个变量值是将链表转换为树的阈值,这个值必须大于2并且至少为8,以满足构造成树的基本要求。
*/
static final int TREEIFY_THRESHOLD = 8;
也就是说,当链表长度达到8时,该链表将会被转换成树结构。转换过程中,Node结点将会变成TreeNode结点。TreeNode结点也是HashMap中维护着的一个内部类。
TreeNode树结点
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; // 结点是否是红色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
...
从这些变量中,可以看到关于红黑树的具体设计。红黑树中提供了hashMap上层API的操作支撑。这部分暂不详解。
回到链表转换为红黑树的具体方法。
treeifyBin方法
提供一个hash值和数组,定位数组中该hash位置上的链表。如果满足条件,则将链表转换为红黑树。
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;
// 循环遍历链表中的结点, 这一步只是将结点Node转为TreeNode,并形成一个双向循环链表。
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); // 另外详解
}
}
整个设计思想大概就是这样了。下面分析一下常用的put、get、remove等方法
put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法主要是调用的putVal()方法。
* @param onlyIfAbsent if true, don't change existing value
* 翻译: 如果该值为true,则不覆盖已存在的value值
*
* @param evict if false, the table is in creation mode.
* 翻译: 如果该值为false,则该表处于创建模式
*
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
// tab 是当前数组table,
// 结点p是数组上hash定位到的Node结点。
// n是数组tab的容量长度
// i是hash定位到的数组下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) // 如果执行操作时是空数组
n = (tab = resize()).length; // 调用resize方法初始化。
if ((p = tab[i = (n - 1) & hash]) == null) // 如果定位到的数组上的下标i的位置,还没有存放元素,p结点就是定位到的头结点。
tab[i] = newNode(hash, key, value, null); // 则创建一个结点存入该处
else { // 如果已经有元素了,则还不清楚目前是链表结构还是红黑树结构
Node<K,V> e; K k;
// 如果p结点已经有元素了,并且key值就是这个p结点key,则将要替换p结点的value值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 这里使用了equals方法,如果不重写目标对象key的equlas方法,将会使用 == 判断两个key,偏离比较key内部的设计本意。
e = p;
// 如果p作为头结点是一个树的根结点,
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果p作为头结点是一个链表头结点
else {
for (int binCount = 0; ; ++binCount) { // binCount 是一个结点计数变量
if ((e = p.next) == null) { // 遍历直到最后一个结点
p.next = newNode(hash, key, value, null); // 到了最后一个结点时,如果binCount >=7 当前已经有7个结点了,则新结点添加后需要转换为红黑树了。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果遍历过程中找到了key值对应的结点,则结束遍历。
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 最后处理这个e结点,如果e结点为空,则上面操作是添加的新结点,putVal操作结束。如果e结点有值,则代表找到了key值对应的结点
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // 通过onlyIfAbsent 判断,是否需要替换key值对应的value值。
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果添加后,触发了hashMap扩容阈值,则要进行resize()操作。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
综上所述,通过putVal方法大概知道了,在hashMap中放入一个元素的过程中,需要考虑以下几点:
- 通过key值计算得到的数组位置上,是否空着。如果空着,则填充。否则进入第二步。
- 如果该位置不为空,是否key值对应的结点?如果不是key值对应的结点,则进入第三步。
- 如果不是key对应的结点,那么该位置上的数据结构是红黑树,还是链表?
- 如果是红黑树,则按红黑树的方式添加一个新结点或者替换value值。如果是链表呢?进入第四步。
- 如果是链表,添加结点后是继续保持链表结构,还是需要转换为红黑树?
- 如果在遍历链表过程中,发现了key值对应的结点,是否必须替换value值?
- 结点添加或者值替换完成后,是否触发HashMap扩容阈值,如果是,则需要resize()。
resize()方法
这个方法是用来重新计算hashMap容量,并且重新定位元素位置的方法。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
// 第一部分初始化变量
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 第二部分算出新的容量和扩容阈值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 从这里可以看出newCap = oldCap << 1,扩容是按2倍扩容
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold // 在装载因子不变的情况下,阈值也是按2倍扩
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 第三部分开始重新定位元素位置
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { // 遍历旧数组中的下标 j 指定的位置上,可能为空,可能有一个,可能有多个(链表或者树)
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 如果有结点,则要判断有几个,是什么数据结构
oldTab[j] = null;
// 如果这已经是最后一个了,也就是下标 j 位置上只有一个结点。
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e; // 从这里可以看出定位方法是一样的,不过容量变了。
// 如果这里有多个结点,并且是红黑树结构
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 如果这里有多个结点,并且是链表结构
else { // preserve order
// 这里的做法,是将原来的一个长链表拆分成两个短链表,定义了两组不同的变量。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表过程中,根据(e.hash & oldCap) == 0 作为条件拆分链表。
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);
// 如果拆完后,loTail代表的短链表不为空,则把这个链表还是放在新数组下标 j 位置上。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 如果拆完后,hiTail代表的短链表不为空,则把这个链表放在 j + oldCap 的下标位置上。
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
在看这个方法过后,可能疑惑和上面的tableSizeFor方法的不同,两者的功能是不一样的。
如果把hashMap看成是一个客栈,则
tableSizeFor重在计算具体容量上,hashMap的数组容量应该是多大,也就是客栈应该修建多少间客房。
resize主要重新拆分和定位数组元素,hashMap扩容后,原来的结点怎么安排,也就是在新客栈修好后,原来住在客房里面的客人,怎么重新安排住下。resize也有扩容,不过没有使用tableSizeFor来计算,因为扩容是在原来基础上进行的,原来容量已经是2的幂了,新容量扩容2倍后还是2的幂,因此无需tableSizeFor来提供计算。
get() 方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value; // hash算法是通过计算key的hashCode,如果对象key的hashCode方法没有重写,将会调用默认的native方法,即使key内部是一样的,由于不同对象的原因,也可能造成hashCode不一样。
}
/**
* 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;
// 如果该hash位置上有结点,则不知道是一个还是多个,是链表还是红黑树
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 检查第一个结点是否匹配
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);
// 如果是链表,则遍历。这也是为什么会尽量避免链表出现的原因。因为通过它寻找结点的时间复杂度是O(n)
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
最后
目前小猿只研究到了这么深,红黑树方面没有做太深入的了解,目前的大多数场景也没用上。其他的等待后续有研究后再补充。