HashMap三百问
1. 定义
HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,源码如下:
1 public class HashMap<K,V> 2 extends AbstractMap<K,V> 3 implements Map<K,V>, Cloneable, Serializable
HashMap是一种支持快速存取的数据结构。
2. 构造函数
HashMap提供了三个构造函数:
-
HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
-
HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
-
HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。
在这里提到了两个参数:初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。
3. HashMap的数据结构
桶:bucket,就是下图中的table数组的每一个成员(橘色),数组的每个位置就叫一个桶,bucket的下标即table数组的下标。Entry<K,V>构成了table数组的项。
HashMap是一个“链表散列”。第一列是table数组,后面是链表。
见源码:下面代码中第23行,表示每创建一个HashMap,就会有一个新的table数组,并且table数组的元素为Entry节点。
1 public HashMap(int initialCapacity, float loadFactor) { 2 //初始容量不能<0 3 if (initialCapacity < 0) 4 throw new IllegalArgumentException("Illegal initial capacity: " 5 + initialCapacity); 6 //初始容量不能 > 最大容量值,HashMap的最大容量值为2^30 7 if (initialCapacity > MAXIMUM_CAPACITY) 8 initialCapacity = MAXIMUM_CAPACITY; 9 //负载因子不能 < 0 10 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 11 throw new IllegalArgumentException("Illegal load factor: " 12 + loadFactor); 13 14 // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。 15 int capacity = 1; 16 while (capacity < initialCapacity) 17 capacity <<= 1; 18 19 this.loadFactor = loadFactor; 20 //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作 21 threshold = (int) (capacity * loadFactor); 22 //初始化table数组 23 table = new Entry[capacity]; 24 init(); 25 }
1 static class Entry<K,V> implements Map.Entry<K,V> { 2 final K key; 3 V value; 4 Entry<K,V> next; 5 final int hash; 6 7 /** 8 * Creates new entry. 9 */ 10 Entry(int h, K k, V v, Entry<K,V> n) { 11 value = v; 12 next = n; 13 key = k; 14 hash = h; 15 } 16 ....... 17 }
其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。
HashMap添加节点的方法
1 /** 2 * HashMap 添加节点 3 * 4 * @param hash 当前key生成的hashcode 5 * @param key 要添加到 HashMap 的key 6 * @param value 要添加到 HashMap 的value 7 * @param bucketIndex 桶,也就是这个要添加 HashMap 里的这个数据对应到数组的位置下标 8 */ 9 void addEntry(int hash, K key, V value, int bucketIndex) { 10 //size:The number of key-value mappings contained in this map. 11 //threshold:The next size value at which to resize (capacity * load factor) 12 //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值 13 // 2.底层数组的bucketIndex坐标处不等于null 14 if ((size >= threshold) && (null != table[bucketIndex])) { 15 resize(2 * table.length);//扩容之后,数组长度变了 16 hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢? 17 bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。 18 } 19 createEntry(hash, key, value, bucketIndex); 20 } 21 22 /** 23 * 这地方就是链表出现的地方,有2种情况 24 * 1,原来的桶bucketIndex处是没值的,那么就不会有链表出来啦 25 * 2,原来这地方有值,那么根据Entry的构造函数,把新传进来的key-value mapping放在数组上,原来的就挂在这个新来的next属性上了 26 */ 27 void createEntry(int hash, K key, V value, int bucketIndex) { 28 HashMap.Entry<K, V> e = table[bucketIndex]; 29 table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e); 30 size++; 31 }
4. HashMap的原理
看原理之前,先把第3个问题看看,了解他的数据结构及相关概念。
5. Hash冲突
就像上面的一个数组的位置上出现了一条链,即一个链表的出现,这就是所谓的hash冲突,即hash值相同。解决hash冲突,就是让链表的长度变短,或者干脆就是不产生链表,一个好的hash算法应该是让数据很好的散列到数组的各个位置,即一个位置存一个数据就是最好的散列,下面说的链地址法,说的就是在hashmap里面冲突的时候,一个节点可以存多个数据。
解决方法见JDK1.8中的3.2节
6. 存储实现:put(key,vlaue)
在写这一节前,我先列出几个问题,看大家都知道否?带着问题去阅读我相信会更深刻。
6.1. HashMap允许为null的原因
6.2. put中有迭代的原因
6.3. 为何数组长度是2^n呢,又为何可以是素数呢
6.4. 扩容问题
6.5. 为什么HashMap中元素的数量越来越多,查找速度越来越慢
6.6. 链表产生问题
6.7. 负载因子loadFactor是否可以大于1
6.8. 为什么要有HashMap的hash()方法,为什么不能直接使用KV中K原有的hash值
下面为大家一一讲解:
put操作的源码如下:
1 public V put(K key, V value) { 2 //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因 3 if (key == null) 4 return putForNullKey(value); 5 //计算key的hash值 6 int hash = hash(key.hashCode()); //------(1) 7 //计算key hash 值在 table 数组中的位置,即对length取模 8 int i = indexFor(hash, table.length); //------(2) 9 //从i处开始迭代 e,找到 key 保存的位置 10 for (Entry<K, V> e = table[i]; e != null; e = e.next) { 11 Object k; 12 //判断该条链上是否有hash值相同的(key相同) 13 //若存在相同,则直接覆盖value,返回旧value(其实还是已经更新的值) 14 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 15 V oldValue = e.value; //旧值 = 新值 16 e.value = value; 17 e.recordAccess(this); 18 return oldValue; //返回旧值 19 } 20 } 21 //修改次数增加1 22 modCount++; 23 //将key、value添加至i位置处 24 addEntry(hash, key, value, i); 25 return null; 26 }
通过源码我们可以清晰看到HashMap保存数据的过程为:
-
首先判断key是否为null:
-
若为null,则直接调用putForNullKey方法;
-
若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置;
-
-
如果table数组在该位置处有元素,则通过比较是否存在相同的key:
-
若存在则覆盖原来key的value;
-
否则将该元素保存在链头(最先保存的元素放在链尾);
-
-
若table在该处没有元素,则直接保存。
这个过程看似比较简单,其实深有内幕。有如下几点:
-
先看迭代处。此处迭代原因就是为了防止存在相同的key值,若发现两个hash值(key)相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这就解释了HashMap中没有两个相同的key。
-
在看(1)、(2)处。这里是HashMap的精华所在。首先是hash方法,该方法为一个纯粹的数学计算,就是计算h的hash值。此处加入了高位运算(见下面代码第19行),避免了由于高位损失而带来的冲突,详见hashmap的hash计算。这也是为什么要有HashMap的hash()方法,难道不能直接使用KV中K原有的hash值的答案。
1 //此处h是hashcode值,Jdk1.7及以前 2 static int hash(int h) { 3 h ^= (h >>> 20) ^ (h >>> 12); 4 return h ^ (h >>> 7) ^ (h >>> 4); 5 }
注:h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。
1 2 //Jdk1.8 3 /** 4 * Computes key.hashCode() and spreads (XORs) higher bits of hash 5 * to lower. Because the table uses power-of-two masking, sets of 6 * hashes that vary only in bits above the current mask will 7 * always collide. (Among known examples are sets of Float keys 8 * holding consecutive whole numbers in small tables.) So we 9 * apply a transform that spreads the impact of higher bits 10 * downward. There is a tradeoff between speed, utility, and 11 * quality of bit-spreading. Because many common sets of hashes 12 * are already reasonably distributed (so don't benefit from 13 * spreading), and because we use trees to handle large sets of 14 * collisions in bins, we just XOR some shifted bits in the 15 * cheapest possible way to reduce systematic lossage, as well as 16 * to incorporate impact of the highest bits that would otherwise 17 * never be used in index calculations because of table bounds. 18 */ 19 static final int hash(Object key) { 20 int h; 21 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//高位参与运算 22 }
-
为什么要有HashMap的hash()方法:详见hashmap的hash计算
此处的hash方法和indexFor方法的参数的来源:
1 2 //计算key的hash值 3 int hash = hash(key.hashCode()); //------(1) 4 //计算key hash 值在 table 数组中的位置,即对length取模 5 int i = indexFor(hash, table.length);
我们知道对于HashMap的table而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素分布均与呢?我们会想到取模,但是由于取模的消耗较大,HashMap是这样处理的:调用indexFor方法。
1 //此处h是hash值,hash值与上(length-1),就相当于是对length取模!!!!太高明了!!! 2 static int indexFor(int h, int length) { 3 return h & (length-1); 4 }
这句话除了上面的取模运算外还有一个非常重要的责任:均匀分布table数据和充分利用空间。
这里我们假设length为16(是2^n形式)和15(不是2^n形式),h为5、6、7。结果如下图:
当length=15时,6和7的结果一样,这样表示他们在table存储的位置是相同的,也就是产生了碰撞冲突,6、7就会在一个位置形成链表,这样就会导致查询速度降低。诚然这里只分析三个数字不是很多,那么我们就看0-15。
从上面的图表中我们看到总共发生了8次碰撞,同时发现浪费的空间非常大,有1、3、5、7、9、11、13、15处没有记录,也就是没有存放数据。这是因为他们在与14进行&运算时,得到的结果最后一位永远都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢。而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。
因为2^n -1得到的二进制数的每个位上的值都为1,那么与全部为1的一一个数进行与操作,速度会大大提升。
所以:所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
综上为什么数组长度是2^n呢?
-
完成取模操作,但是&速度比%更快(h&(length-1));
-
使得table数据均匀分布,非(2^n-1)总有某位是0,&操作后,该位会造成hash冲突;
-
充分利用空间,尽量没有hash冲突,尽可能地均匀分布;
另外,需要说明的是:在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,具体证明可以参考为什么一般hashtable的桶数会取一个素数 这篇文章,简单来说就是采用2^n方法,高位可能会失效。比如说取2^3=8,H( 11100(二进制) ) = H( 28 ) = 4,H( 10100(二进制) ) = H( 20 )= 4,就出现了冲突。这就是为什么Hashtable初始化桶大小为11,就是桶大小设计为素数的应用(但是,Hashtable扩容后不能保证还是素数)。HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。在这里我们暂不讨论HashMap和Hashtable的区别,后面会有详细介绍。
这里我们再来复习put的流程:当我们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。具体的实现过程见addEntry方法,如下:
1 //bucketIndex即table数组下标 2 void addEntry(int hash, K key, V value, int bucketIndex) { 3 //获取bucketIndex处的Entry 4 Entry<K, V> e = table[bucketIndex]; 5 //将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 6 table[bucketIndex] = new Entry<K, V>(hash, key, value, e); 7 //若HashMap中元素的个数超过极限了,则容量扩大两倍 8 if (size++ >= threshold) 9 resize(2 * table.length); 10 }
在这里需要注意两个问题:
-
链的产生:
这是一个非常优雅的设计。系统总是将新的Entry对象添加到桶处即bucketIndex处。如果bucketIndex处已经有了对象(也就是说table[bucketIndex]这个可以取到对象),那么新添加的Entry对象将指向原有的Entry对象(这个也好理解,再去看看Entry,这个内部类的一个属性next,就是链表里面的指针不是嘛),形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是table[bucketIndex]==null,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。也就没有冲突啦,我上面又把这个代码补充到那个Entry代码的下面啦。我又贴到下面。
-
扩容问题:
随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度(为啥呢?原来是直接找到数组的index就可以直接根据key取到值了,但是冲突严重,也就是说链表长,那就得循环链表了,时间就浪费在循环链表上了,也就慢了),为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当【HashMap中元素的数量==table数组长度*加载因子】。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理(为啥呢?原来,扩容后数组长度变了,而数组的下标跟数组长度有关(h&(length-1)),故得重算。)。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。这个我也在上面链表产生的过程中写了详细的注释
另外就是,扩容的时候也是二倍扩容,也是因为这个道理,要满足数组长度是2^n,这就又回到上一个问题了。
1 2 /** 3 * HashMap 添加节点 4 * 5 * @param hash 当前key生成的hashcode 6 * @param key 要添加到 HashMap 的key 7 * @param value 要添加到 HashMap 的value 8 * @param bucketIndex 桶,也就是这个要添加 HashMap 里的这个数据对应到数组的位置下标 9 */ 10 void addEntry(int hash, K key, V value, int bucketIndex) { 11 //size:The number of key-value mappings contained in this map. 12 //threshold:The next size value at which to resize (capacity * load factor) 13 //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值 14 // 2.底层数组的bucketIndex坐标处不等于null 15 if ((size >= threshold) && (null != table[bucketIndex])) { 16 resize(2 * table.length);//扩容之后,数组长度变了 17 hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢? 18 bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。 19 } 20 createEntry(hash, key, value, bucketIndex); 21 } 22 23 /** 24 * 这地方就是链表出现的地方,有2种情况 25 * 1,原来的桶bucketIndex处是没值的,那么就不会有链表出来啦 26 * 2,原来这地方有值,那么根据Entry的构造函数,把新传进来的key-value mapping放在数组上,原来的就挂在这个新来的next属性上了 27 */ 28 void createEntry(int hash, K key, V value, int bucketIndex) { 29 HashMap.Entry<K, V> e = table[bucketIndex]; 30 table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);//Entry.next=e; 31 size++; 32 }
-
负载因子loadFactor:
先看一下HashMap的几个字段:
1 int threshold; // 所能容纳的key-value对极限,取值threshold = length * LoadFactor 2 final float loadFactor; // 负载因子 3 int modCount; 4 int size; //HashMap中实际存在的键值对数量
首先,Entry[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * LoadFactor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改。除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳最大键值对数量threshold的区别。
而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。
7. 读取实现:get(key)
相对于HashMap的存而言,取就显得比较简单了。通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。
1 public V get(Object key) { 2 // 若为null,调用getForNullKey方法返回相对应的value 3 if (key == null) 4 return getForNullKey(); 5 // 根据该 key 的 hashCode 值计算它的 hash 码 6 int hash = hash(key.hashCode()); 7 // 取出 table 数组中指定索引处的值 8 for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { 9 Object k; 10 //若搜索的key与查找的key相同,则返回相对应的value 11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 12 return e.value; 13 } 14 return null; 15 }
在这里能够根据key快速的取到value除了和HashMap的数据结构密不可分外,还和Entry有莫大的关系,在前面就提到过,HashMap在存储过程中并没有将key,value分开来存储,而是当做一个整体key-value来处理的,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。
8. 多线程下的HashMap出现的问题
-
多线程put操作后,get操作导致死循环导致cpu100%的现象。主要是多线程同时put时,如果同时触发了rehash操作,会导致扩容后的HashMap中的链表中出现循环节点进而使得后面get的时候,会死循环。
-
多线程put操作,导致数据丢失,也是发生在个线程对hashmap 扩容时。
JDK1.8之HashMap
JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等;
1. 数据结构
从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。
在Jdk1.7中存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。本文不再对红黑树展开讨论,想了解更多红黑树数据结构的工作原理可以参考初步了解红黑树
2. 功能与方法
2.1. 确定Hash桶数组索引位置
源码如下:
1 方法一: 2 static final int hash(Object key) { //jdk1.8 & jdk1.7 3 int h; 4 // h = key.hashCode() 为第一步 取hashCode值 5 // h ^ (h >>> 16) 为第二步 高位参与运算 6 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 7 } 8 方法二: 9 static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的 10 return h & (length-1); //第三步 取模运算 11 }
这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
举例说明,其中n为table的长度。
补充点知识:
bucket 单词意思:桶;
Entry 内部类;
“>> 右移,高位补符号位” 这里右移一位表示除2;
“>>> 无符号右移,高位补0”; 与>>类似“;
<< 左移” 左移一位表示乘2,二位就表示4,就是2的n次方;
^ 异或:相同为0,不同为1
2.2. put()方法(含红黑树)
JDK1.8HashMap的put方法执行过程可以通过下图来理解:
这里对每一步进行一下解释:
-
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
-
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
-
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
-
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
-
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
-
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
JDK1.8HashMap的put方法源码如下:
1 public V put(K key, V value) { 2 //hash()方法在上面已经出现过了,就不贴了 3 return putVal(hash(key), key, value, false, true); 4 } 5 6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 7 boolean evict) { 8 Node<K, V>[] tab; 9 Node<K, V> p; 10 int n, i; 11 // 步骤①:tab为空则创建 12 if ((tab = table) == null || (n = tab.length) == 0) 13 n = (tab = resize()).length; 14 // 步骤②:计算index,并对null做处理 15 if ((p = tab[i = (n - 1) & hash]) == null) 16 tab[i] = newNode(hash, key, value, null); 17 else { 18 Node<K, V> e; 19 K k; 20 // 步骤③:节点key存在,直接覆盖value 21 if (p.hash == hash && 22 ((k = p.key) == key || (key != null && key.equals(k)))) 23 e = p; 24 // 步骤④:判断该链为红黑树 25 else if (p instanceof TreeNode) 26 e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); 27 // 步骤⑤:该链为链表 28 else { 29 for (int binCount = 0; ; ++binCount) { 30 if ((e = p.next) == null) { 31 p.next = newNode(hash, key, value, null); 32 //链表长度大于8转换为红黑树进行处理 TREEIFY_THRESHOLD = 8 33 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 34 treeifyBin(tab, hash); 35 break; 36 } 37 // key已经存在直接覆盖value 38 if (e.hash == hash && 39 ((k = e.key) == key || (key != null && key.equals(k)))) 40 break; 41 p = e; 42 } 43 } 44 if (e != null) { // existing mapping for key 45 V oldValue = e.value; 46 if (!onlyIfAbsent || oldValue == null) 47 e.value = value; 48 afterNodeAccess(e); 49 return oldValue; 50 } 51 } 52 ++modCount; 53 // 步骤⑥:超过最大容量 就扩容 threshold:单词解释--阈(yu)值,不念阀(fa)值!顺便学下语文咯。 54 if (++size > threshold) 55 resize(); 56 afterNodeInsertion(evict); 57 return null; 58 }