本篇文章我们主要来看看Map的相关内容。
需要学习Collection详解的可以查看我的另一篇文章:java集合之Collection
Map
1.1 Map 简介
什么是Map?
Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键对象和一个值对象。其中,键对象不允许重复,而值对象可以重复,并且值对象还可以是 Map 类型的,就像数组中的元素还可以是数组一样。
特点:该集合存储键(key)值(value)对,一对一对往里存,而且要保证键(key)的唯一性。
什么时候使用Map集合:当数据之间存在映射关系时,优先考虑Map集合。
1.2 Map 框架示意图
1.3 Map常用方法
- V put(K key, V value):将指定的值与此映射中的指定键关联,添加键值对。
- void putAll(Map<? extends K,? extends V> m):从指定映射中将所有映射关系复制到此映射中,批量添加键值对。
- void clear():从此映射中移除所有映射关系,清空所有键值对。
- V remove(Object key):如果存在一个键的映射关系,则将其从此映射中移除,删除单个键值对。
- boolean containsKey(Object key):如果此映射包含指定键的映射关系(是否包含该键),则返回 true。
- boolean containsValue(Object value):如果此映射将一个或多个键映射到指定值(是否包含该值),则返回 true。
- boolean isEmpty():如果此映射未包含键-值映射关系,该map集合为空,则返回 true。
- V get(Object key):返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
- int size():返回此映射中的键-值映射关系(键值对)数
- Collection values():返回此映射中包含的值的 Collection 视图(集合)。
代码示例:
import java.util.*;
public class MapDemo {
public static void main(String[] args) {
Map<Integer, Integer> map1 = new HashMap<Integer, Integer>();
Map<Integer, Integer> map2 = new HashMap<Integer, Integer>();
map1.put(1, 100);
map1.put(2, 200);
map1.put(3, 300);
map2.put(11, 1100);
map2.put(12, 1200);
map2.put(13, 1300);
// 查看包含的元素个数
System.out.println(map1.size());
System.out.println(map1);
// 存入新的key为1的数据,覆盖原(1,100)
map1.put(1,99);
System.out.println(map1);
// 根据Key获取Value
System.out.println("map1 get key 2 is: "+map1.get(2));
// 判断是否包含某个Key或则Value
System.out.println("map1中有key为4的元素? "+map1.containsKey(4));
System.out.println("map1中有Value为300的元素? "+map1.containsValue(300));
// 将map2中的元素复制到map1中
map1.putAll(map2);
System.out.println("after copy, map1 now is: "+map1);
System.out.println("after copy, map2 now is: "+map2);
// 从map2中移出一组键值对(通过key)
map2.remove(12);
System.out.println("After remove 12, map2 is: "+map2);
// 清空map2
map2.clear();
System.out.println("After clear, map2 is: "+map2+" and size is: "+map2.size());
}
}
运行结果:
3
{1=100, 2=200, 3=300}
{1=99, 2=200, 3=300}
map1 get key 2 is: 200
map1中有key为4的元素? false
map1中有Value为300的元素? true
after copy, map1 now is: {1=99, 2=200, 3=300, 11=1100, 12=1200, 13=1300}
after copy, map2 now is: {11=1100, 12=1200, 13=1300}
After remove 12, map2 is: {11=1100, 13=1300}
After clear, map2 is: {} and size is: 0
1.4 Map的两种取出方式:
Map集合的取出原理:将Map集合转成Set集合,再通过迭代器取出。
- Set keySet():返回此映射中包含的键的 Set 视图(集合)。
将map中所有的键存入到Set集合,因为set具备迭代器,所有迭代方式取出所有的键再根据get()方法 ,获取每一个键对应的值
代码示例:
public class MapDemo2 {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
map.put("01", "zhangsan1");
map.put("02","zhangsan2");
map.put("03", "zhangsan3");
map.put("04", "zhangsan4");
//先获取map集合的所有键的set集合,keySet();
Set<String> k = map.keySet(); //Set<String>相当于返回值类型
//有了Set集合,就可以获取其迭代器.(注意Set集合的类型要和迭代器保持一致)
Iterator<String> it = k.iterator();
while(it.hasNext()){
String key = it.next();
//有了键,就可以通过map集合的get方法获取对应的值
String value =map.get(key);
System.out.println("key:"+key+"---value:"+value);
}
}
}
运行结果:
key:01---value:zhangsan1
key:02---value:zhangsan2
key:03---value:zhangsan3
key:04---value:zhangsan4
代码示例:
public class MapDemo3 {
public static void main(String[] args) {
Hashtable<String , String> hashTable;
TreeMap<String , String> treeMap;
HashMap<String , String> hashMap;
LinkedHashMap<String, String> linkedHashMap;
for(int i = 0; i < 10; i++) {
hashTable.put(String.valueOf(i), i + "" + i);
treeMap.put(String.valueOf(i), i + "" + i);
hashMap.put(String.valueOf(i), i + "" + i);
linkedHashMap.put(String.valueOf(i), i + "" + i);
}
System.out.println("Hashtable: " + hashTable.keySet());
System.out.println("TreeMap: " + treeMap.keySet());
System.out.println("HashMap: " + hashMap.keySet());
System.out.println("LinkedHashMap: " + linkedHashMap.keySet());
}
运行结果:
Hashtable: [9,8,7,6,5,4,3,2,1,0]
TreeMap: [0,1,2,3,4,5,6,7,8,9]
HashMap: [0,1,2,3,4,5,6,7,8,9]
LinkedHashMap: [0,1,2,3,4,5,6,7,8,9]
- Set<Map.Entry<K,V>> entrySet():返回此映射中包含的映射关系的 Set 视图(集合)。
将Map集合中的映射关系存入到了Set集合中,而这个映射关系的数据类型就Map.Entry。
Map.Entry:其实Entry也是一个接口,它是Map接口中的一个内部接口,getKey()和getValue是接口Map.Entry<K,V>中的方法,返回对应的键和对应的值
代码示例:
public class MapDemo4 {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
map.put("02", "zhangsan2");
map.put("03", "zhangsan3");
map.put("01", "zhangsan1");
map.put("04", "zhangsan4");
//将Map集合中的映射关系取出,出入到Set集合中
Set<Map.Entry<String,String>> es = map.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();
while(it.hasNext()){
Map.Entry<String, String> mey = it.next();
//getKey()和getValue是接口Map.Entry<K,V>中的方法,返回对应的键和对应的值
String key = mey.getKey();
String value = mey.getValue();
System.out.println(key+":"+value);
}
}
}
运行结果:
04:zhangsan4
01:zhangsan1
02:zhangsan2
03:zhangsan3
常见问题:
Entry是接口 ,但是为什么不定义定义在外面呢?
原因:
Entry代表的是映射关系,先有Map集合,才有的映射关系,所以它是Map集合内部的事物,因此将Entry定义为在Map的内部集合,可以直接访问Map集合中的元素
看到这里的时候大伙儿估计都明白了为什么HashMap为什么要选择Entry数组来存放key-value,因为Entry实现的Map.Entry接口里面定义了getKey(),getValue(),setKey(),setValue()等方法,对键值对进行了一个封装便于后面的操作。
HashMap
2.1 HashMap简介
HashMap是Map的常见实现类,也是Java中使用最频繁的储存键值对的结构,即散列表,它存储的是键值对(key-value)映射。HashMap 继承于AbstractMap。
2.2 HashMap的工作原理
Java中的数组在添加或者删除的时候,都会复制一个新数组,比较耗内存,但是数组遍历比较高效(寻址容易,插入和删除困难);然而,链表则是遍历比较慢,而添加和删除元素代价低(寻址困难,插入和删除容易)。HashMap则是巧妙的结合这两点——哈希表
HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些键值对(Entry)分散存储在一个数组中,这个数组就是HashMap的主干。简单的来说HashMap是由数组+链表组成的。下面我们可以通过HashMap的结构图来看到它的基本结构。
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。
上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。我们存储的是key和value。其中hash通过key计算的hashcode值,next是指向下一个节点。由此可以理解存储原理了(详细看源码分析)。
2.3 HashMap的源码分析
2.3.1 重要属性
-
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 : 默认的初始容量 (16)
-
static final int MAXIMUM_CAPACITY = 1 << 30 : 最大容量
-
static final float DEFAULT_LOAD_FACTOR = 0.75f : 负载因子:该属性是表示在扩容之前容量占有率的一个标尺。它控制数组存放Node是的疏密程度。loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,查找效率降低。loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀。默认的0.75f是一个权衡后的值。
-
Entry[] table : Entry数组类型,HashMap的键值对都是存储在Entry数组中
-
int threshold; : 阈值。用于判断是否需要调整HashMap的容量。threshold = 容量 * 加载因子,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
-
static final int TREEIFY_THRESHOLD = 8:如果链表超过了这个值,则将单链表转变为红黑树。
2.3.2 构造函数
// 构造函数一
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
// 构造函数二
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 构造函数三
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);
}
// 构造函数四
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
我们用的最多的就是第一个无参构造方法,默认的初始容量(16)和加载因子(0.75)。
构造函数二调用的构造函数三,手动设置初始容量大小。
构造函数三是手动设置初始容量和负载因子。
构造函数是将别的map映射到自己的map中。
2.3.3 存储数据put
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
//搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改次数+1
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
我们看看putForNullKey(value)方法:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) { //如果有key为null的对象存在,则覆盖掉
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0); //如果键为null的话,则hash值为0
return null;
}
如果key为null的话,hash值为0,对象存储在数组中索引为0的位置。即table[0];
我们再看来一下如何通过key的hashCode值计算hash码:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
得到hash码之后就会通过hash码去计算出应该存储在数组中的索引,计算索引的函数如下:
static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
return h & (length-1); //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出,&运算时当同时为1,结果为1,否则为0
}
我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。
根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:
- 如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。
- 如果这两个 Entry 的 key通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。
- 如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部
addEntry() 方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; //取得数组中索引为bucketIndex的Entry对象
table[bucketIndex] = new Entry<>(hash, key, value, e);//用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。
if (size++ >= threshold) //判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容
resize(2 * table.length); //HashMap扩容是扩为原来的两倍。
}
2.3.4 数据读取 get
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
2.3.5 调整大小
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);//用来将原先table的元素全部移到newTable里面
table = newTable; //再将newTable赋值给table
threshold = (int)(newCapacity * loadFactor);//重新计算临界值
}
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
当HashMap中的元素个数超过 数组大小 * loadFactor 时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过 16 * 0.75 =12的时候,就把数组的大小扩展为 2 * 16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
Java8 中的HashMap
3.1 Java8 中HashMap的工作原理
JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的。
3.2 Java8 中HashMap的源码分析
3.2.1 put() 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第四个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 数组该位置有数据
Node<K,V> e; K k;
// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 到这里,说明数组该位置上是一个链表
for (int binCount = 0; ; ++binCount) {
// 插入到链表的最后面(Java7 是插入到链表的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.2.2 数组扩容
resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移。
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 &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将阈值扩大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
newCap = oldThr;
else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候
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;
// 用新的数组大小初始化新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可
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)
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树,具体我们就不展开了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 这块是处理链表的情况,
// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的
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;
// 第二条链表的新的位置是 j + oldCap,这个很好理解
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3.2.3 get() 方法
计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步遍历链表,直到找到相等(==或equals)的 key.
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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) {
// 判断第一个节点是不是就是需要的
if (first.hash == hash && // always check first node
((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);
}
}
return null;
}