【Java从头开始到光头结束】No9.Map集合之HashMap回顾(附JDK 1.8 源码)

Map集合之HashMap,附JDK 1.8 源码

基础回顾 → 集合之HashMap
————————————————————————————————————
文章部分内容选自书本《码出高效:Java开发手册》
相关内容:
【Java从头开始到光头结束】No7.回顾Map集合基础与树形数据结构
【Java从头开始到光头结束】No8.Map集合之TreeMap回顾(附JDK 1.8 源码)

1.前言

HashMap在JDK 1.8 之前的并发问题我们就不再分析了,技术人员向前看,别老做考古学家,话虽如此,但是前车之鉴后车之师,加上这也是面试可能会经常问到的一部分内容,这里就简单总结一下,就不再详细去看1.7的源码了。(《码出高效:Java开发手册》书中也对1.7的死链和扩容数据丢失做了详细说明,有兴趣的朋友可以了解一下)

关于在1.7代码中并发下的扩容死循环,或者说死链问题:
1.7代码中,HashMap底层是数组加链表,扩容时采用先扩容,后插入的方式,插入链表的方式为头插法,最新插入的元素节点总是在数组上,也就是链表的头上,并发扩容访问下可能会造成A指向B,B又指向A的情况,当get()方法访问到这一块,就会陷入死循环。

在1.8代码中,HashMap底层是数组加链表和红黑树,扩容时采用先插入,后扩容的方式,插入链表的方式为尾插法,最新插入的元素节点总是在链表的最后,解决了链表的死循环问题,但是在并发下红黑树旋转平衡的代码中还是会发生死循环问题。毕竟HashMap不是线程安全的,福无双至祸不单行。

至于1.7和1.8版本间的HashMap区别,建议大家看这篇博客,很详细
(1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析
还有1.8版本中的红黑树旋转死循环问题,参考以下博客
JDK8中HashMap依然会死循环!

这里我们对之前版本的内容就说到这里,点到为止。

2.整体回顾HashMap

在这里插入图片描述
总的来说HashMap在我们的集合大家族中还是比较单纯的,属于一种原生态的哈希数据结构实现。这里首先第一个想要强调的点是,之前在我们的ArrayList源码拓展分析中也提到过,对于底层是有数组的这种数据集合,因为数组的长度不可变性,导致在集合扩容时需要重新拷贝值到新数组中,如果没有显式的先设置好初始容量,之后在添加元素的时候效率就会偏低,具体我们在源码分析的时候再详细说明。

概括一下,HashMap中Key-Value值都是可以为null的,HashMap是线程不安全的,1.8以后底层的实现是数组加链表和红黑树,具体底层的结构我们来看一下相关概念:
在这里插入图片描述在这里插入图片描述
所有哈希桶中的元素个数合就是HashMap的Size。没啥总结的,直接看代码得咯。

3.浅析HashMap源码

老规矩,看源码的第一件事,看类上的注释,这个我们挑其中一小部分看一下:

it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
如果迭代性能很重要,那么不要设置太高的初始容量(或者太低的负载系数)。
———————————————————————————————————
Note that this implementation is not synchronized
请注意,此实现不同步
推荐同步方式:Map m = Collections.synchronizedMap(new HashMap(…));
———————————————————————————————————
fail-fast: if the map is structurally modified at any time after
the iterator is created, in any way except through the iterator’s own
remove method,the iterator will throw a {@link ConcurrentModificationException}.
Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

fail fast:如果在迭代器创建之后的任何时候对map集合的结构进行了修改,
无论以任何方式,调用除了通过迭代器自己的remove方法,迭代器将抛出
{@link ConcurrentModificationException}。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是在将来某个不确定的时间冒着任意的、不确定的行为的风险。
———————————————————————————————————
注意fail fast是一种集合中常见的错误检测机制,通常使用在遍历集合元素的过程中,总结一下上边的话,意思就是,我先创建了迭代器,还没迭代完,集合的元素被其他方法给调用remove了(这个remove方法不是当前迭代器的remove方法),就会报错,因为他不愿意冒风险,此时遍历的元素可能已经不是最新的元素集了。

我们看一下书上是如何描述fail-fast机制的
在这里插入图片描述
下来我写点代码看一下这个问题:

public class ReviewHashMap {
    
    

	public static void main(String[] args) {
    
    
		
		HashMap<String, String> hp = new HashMap<>();
		hp.put("a1", "value one"); 
		hp.put("a2", "value twe");
		hp.put("a3", "value three"); 
		hp.put("a4", "value four");
		
		Iterator<String> it = hp.keySet().iterator();
		System.out.println("it ----------------------------------");
		System.out.println("hp.size(): " + hp.size());
		while (it.hasNext()) {
    
    
			System.out.println(it.next());
		}
		
		System.out.println();
		System.out.println("it2 ----------------------------------");
		System.out.println("hp.size(): " + hp.size());
		Iterator<String> it2 = hp.keySet().iterator();
		while (it2.hasNext()) {
    
    
			String key = it2.next();
			System.out.println(key);
			if ("a3".equals(key)) {
    
    
				it2.remove();
			}
		}
		
		System.out.println();
		System.out.println("it3 ----------------------------------");
		System.out.println("hp.size(): " + hp.size());
		Iterator<String> it3 = hp.keySet().iterator();
		while (it3.hasNext()) {
    
    
			System.out.println(it3.next());
			hp.remove("a2");
		}
		
	}

}

直接看控制台打印:
在这里插入图片描述
第三次迭代中报错了,原因就是使用代码hp.remove(“a2”);是迭代器以外的remove方法,导致迭代器发现修改次数不一致,为了安全起见报出错误。在阿里的开发手册中也推荐了,如果你需要在集合的遍历中删除元素,建议你使用迭代器遍历的方式以及使用迭代器的remove()方法
———————————————————————————————————
一不留神有点跑偏了0.0,下来回到我们源码中,先看看属性值:

/**
 * The default initial capacity - MUST be a power of two.
 * 默认的初始容量为1左移4位 → 二进制10000 → 值为16
 * 值必须为2的冥次方,这个要求和后边扩容计算hash值有关系。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大容量,1左移30位,值必须也为2的冥次方。
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * The load factor used when none specified in constructor.
 * 负载因子:0.75
 * 首先说一点就是hashmap的扩容和那种传统的数组扩容不一样,传统数组是完全存不下了才扩容的,
 * HashMap是,假设容量为16,负载因子为0.75,那扩容触发条件为16 * 0.75 = 12
 * 当HashMap的数组上容量超过12就会扩容,而不是等到了16才扩容。
 * 因为HashMap是哈希结构的集合,对应哈希算法理想情况下是,不同的key对应不同的Hash值,但是
 * 事实上会有哈希值冲突的情况存在,所以,在这种情况下如何设置容量大小和负载因子,
 * 既可以不浪费空间,又可以适当减少冲突的发生,就涉及到了概率统计学。。。
 * 具体和泊松分布 概率统计有关系,源码只是实现,不涉及原理,这个在源码的注释中有泊松分布
 * 详细说明的连接,有兴趣的朋友可以再去了解一下。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

在这里插入图片描述
下来继续看源码中的属性值:

/**
 * 我们都知道现在1.8后的HashMap底层是数组,上边是链表或者红黑树,1.7是数据加链表
 * 这就牵扯到一个何时使用链表,何时使用红黑树的问题,默认先使用链表,等到链表中数据超过某个阈值
 * 就会当前这个值:8 的时候,会将当前链表中的元素节点全部转化为红黑树节点。
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 上边是链表转红黑树的条件值,叫TREEIFY,而这里UNTREEIFY就是反向了,是从红黑树退化到链表
 * 的阈值了:6 这里简单说一下为啥是8和6,翻阅许多资料和博客后,觉得有两种原因:
 * 1.之前的博客中有提到过关于时间复杂度的问题,这里主要也是因为这两个不同数据结构
 * 在不同的数据量下,查找的时间复杂度的区别
 * 当节点为6个 平均查询时间复杂度:链表→6/2=3,红黑树为log 6≈2.6,差别不大
 * 当节点为8个 平均查询时间复杂度:链表→8/2=4,红黑树为log 8≈3,有一定差别了
 * 这里是一个折中选择,原因2是如下图
 */
static final int UNTREEIFY_THRESHOLD = 6;

源代码中说明,如果正常遵循泊松分布,那么一个哈希桶中,存在8个以上元素的概率极低,为0.00000006,很难发生这种情况,但如果万一发生了,就得考虑到查找时间优化问题,就是上述的原因1
在这里插入图片描述

/**
 * 关于链表转树话还有一个限制条件就是当前HashMap的集合元素要大于64,否则不转,这又是为啥呢
 * 如果集合元素数据太小,而哈希算法又机缘巧合的将一个哈希桶中元素防止过多超过8个
 * 此时扩容才是减少哈希冲突的最好方式,而着重点不在于此时如何优化链表和红黑树的查询效率上。
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
* HashMap中存储元素节点的数组。
*/
transient Node<K,V>[] table;

/**
* 上边有说过扩容的阈值:
* 默认值为 (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
* 在这里说明一下为什么HashMap同ArrayList一样,最好显式设置初始值大小,因为默认容量都比较小
* 后边添加比较多的元素都得扩容,这里默认第一次扩容阈值为12,扩容成之前的两倍(后边会看到源码)
* 需要被动扩容7次才能放下1000个元素,虽然比ArrayList扩容次数少,但是如果一开始能明确
* 数据集合的大小就最好不过了。
*/
int threshold;

/**
* DEFAULT_LOAD_FACTOR 0.75 是默认值,这个才是本尊。
*/
final float loadFactor;

下来像这种迭代器,或者说Values啊KeySet这种每个Map里都有的就不看了
在这里插入图片描述
我们挑着看两个内部类,首先是Node<K,V>:
和就是我们HashMap中链表的元素节点,我们知道LinkedList底层也是由链表实现的,不过LinkedList使用的是双向链表 → 持有前后两个链表元素节点的引用
HashMap是使用的是单向链表 → 持有后一个链表元素节点的引用 看图
在这里插入图片描述
下来简单看一下 TreeNode<K,V>,都是红黑树结构的基本属性:

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;
     // needed to unlink next upon deletion → 删除后需要取消链接
     // prev 是前节点的意思,不符合红黑树概念,暂时理解为链表和红黑树之间转换需要用到的值
     TreeNode<K,V> prev;
     // 节点颜色
     boolean red;
     // 构造方法,使用父类LinkedHashMap.Entry<K,V>的构造
     TreeNode(int hash, K key, V val, Node<K,V> next) {
    
    
         super(hash, key, val, next);
     }
     // 省略其他方法
}

下来我们看一下构造方法:

/**
* 默认无参的构造方法,只在内部初始化了【负载因子】
*/
public HashMap() {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
* 参数为初始容量的构造方法,调用了下边两个参数的构造方法。
*/
public HashMap(int initialCapacity) {
    
    
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* 两个参数分别为【初始容量】和【负载因子】的构造方法,做了些基本的判断
* 这里我想说的是最后一句,首先声明一下,HashMap是在put()方法调用的时候才初始化数组的
* 它并没有在构造方法中就直接把数据New了出来,算是一种延迟构造的方式,但是有个问题
* 总得有个变量存一下传进来的【初始容量】值,后边初始化数组的时候才能使用啊,没错
* 它给怼在threshold里边了,构造时的threshold暂时不是我们上边说的扩容阈值,而是初始容量值。
* 这个我们在后边看put方法的时候可以看到threshold的使用。
* 至于tableSizeFor()方法是干嘛的,我们看下边↓
*/
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);
}

/**
 * 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;
}

/**
* 参数为一个map集合的构造方法。负载因子依旧是默认值,我们下边主要看putMapEntries()方法。
*/
public HashMap(Map<? extends K, ? extends V> m) {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

/**
* 首先这个evict 此值是putVal()方法要使用到的,其官方注释拷贝过来了
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
* 当evict=false 标识正在初始化map集合,
* 当evict=true 是已经初始过了,在之前的基础上追加参数map集合的所有元素
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    
    
    int s = m.size();
    if (s > 0) {
    
    
        if (table == null) {
    
     // pre-size
        	// ① 判断传进来的集合长度是否超过最大值,获得初始化容量值,放在threshold中
            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();
        // 循环put,这个putVal()后边我们会详细看
        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);
        }
    }
}

现在大致上对于HashMap的结构属性和初始化方法有了简单的认识,下来我们就看一下put()方法,其中putVal()方法中几乎涵盖调用了HashMap里所有比较关键的方法,我们将HashMap的源码由此方法逐一展开:

// 主要的实现在putVal()方法中
public V put(K key, V value) {
    
    
    return putVal(hash(key), key, value, false, true);
}

// 主要的实现也是在putVal()方法中,不过是第四个参数不一致
public V putIfAbsent(K key, V value) {
    
    
    return putVal(hash(key), key, value, true, true);
}

/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* Absent:缺席 值为true就只是插入,不更改,为false会更改原值,参考putIfAbsent()方法
* @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;
   // ① 第一次初始化hashMap集合,这里resize()既可以扩容也可以初始化
   if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length;
   // ② 这里的tab[i = (n - 1) & hash])为数组对应下标元素,如果没有,就直接把新传入的键值对插入到对应数组下标位置
   if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);
   else {
    
    
       // 经过上两步的判断,此时到达这里已经说明HashMap集合已经初始化过了,
       // 并且hash对应的数组下标位置已经有元素存在,此时需要进一步判断
       // Node<K,V> e 就是existing mapping for key,找到对应一致的key元素节点
       Node<K,V> e; K k;
       // 这里判断hash对应的数组下标位置的元素key和我们新插入的key是否一致
       // 一致的话就将其返回,后边会做处理
       // 这第一个判断没有进去,那说明此时需要插入的肯定为链表或红黑树结构
       if (p.hash == hash &&
           ((k = p.key) == key || (key != null && key.equals(k))))
           e = p;
       // 这里先判断是不是红黑树结构,如果是,调用红黑树特有的put方法
       else if (p instanceof TreeNode)
           e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
       // 进到这个else中,那肯定当前是链表结构
       else {
    
    
           // 开始循环这个链表结构
           for (int binCount = 0; ; ++binCount) {
    
    
               // 如果这个链表当前元素的一下个元素为空
               if ((e = p.next) == null) {
    
    
                   // 则插入新的键值对
                   p.next = newNode(hash, key, value, null);
                   // 如果当前循环的计数值大于等于7,那加上我们即将插入的元素,就达到了8个
                   // 需要链表转红黑书,调用treeifyBin()方法
                   if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                       treeifyBin(tab, hash);
                   break;
               }
               // 如果在循环链表途中发现了key值一样的元素节点,就同第一个判断一样
               // 将对应key值一样的元素节点赋值返回。
               if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   break;
               p = e;
           }
       }
       // 开始判断是否找到了对应Hash和Key值一样的元素
       if (e != null) {
    
     // existing mapping for key
           // 这里需要将被替换的旧值返回,不过好像一般没人用到这个值。
           V oldValue = e.value;
           // 根据onlyIfAbsent和旧值是否为空值做进一步 可否覆盖值的判断 
           if (!onlyIfAbsent || oldValue == null)
               e.value = value;
           // 这个是扩张方法,留给我们自己实现拓展的。
           afterNodeAccess(e);
           // 最后返回旧元素值
           return oldValue;
       }
   }
   ++modCount;
   // 判断是否需要扩容
   if (++size > threshold)
       resize();
   // 这个也是一个扩张方法,留给我们自己实现拓展的。
   afterNodeInsertion(evict);
   return null;
}

经过上边putVal()方法的阅读,相信我们对HashMap操作整体已经有了一个概念,下来我们进一步看一下其中的一些比较重要的方法,毋庸置疑,我们这里先看resize()方法

/**
* Initializes or doubles table size.
* 源码注释我就留了这一句,表明这个方法既可以初始化集合又可以扩容,并且扩容大小为两倍
*
* @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;
       }
       // 2的冥次方值,左移一位可以理解为变成其两倍值,例如16的二进制位:10000
       // 右移一位变成100000,其值为32
       // 这里就是进行扩容了,变成两倍。
       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
           newThr = oldThr << 1; // double threshold
   }
   // 初始化方法分支1:使用带参数int initialCapacity的构造方法
   // 这里oldThr就是我们的threshold,在构造方法说明过,暂时存的是初始容量值
   else if (oldThr > 0) // initial capacity was placed in threshold
       newCap = oldThr;
   else {
    
                   // zero initial threshold signifies using defaults
       // 初始化方法分支2:使用的是无参的构造方法,容量和负载因子都是默认初始值
       newCap = DEFAULT_INITIAL_CAPACITY;
       newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
   }
   // 这里其实是为【初始化方法分支1】做了后续的扩容阈值的计算工作,因为
   // 【初始化方法分支1】里边只做了newCap的赋值,没有赋值newThr
   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) {
    
    
           Node<K,V> e;
           // 数组下标位置有值(默认为null)
           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 {
    
     // preserve order 维护秩序
                   // 链表的尾插法↓
                   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;
                       newTab[j + oldCap] = hiHead;
                   }
               }
           }
       }
   }
   return newTab;
}

下边比较重要的一个红黑树对应的节点插入方法putTreeVal()就不看了,这个说明起来太麻烦了,没有红黑树结构基础很难看懂那段代码,有兴趣的朋友可以先去了解红黑树的基础知识和其维持平衡的左右旋转,了解之后再看代码就能好很多,依旧推荐这本《码出高效:Java开发手册》,以及treeifyBin()方法,将链表转红黑树,也是和putTreeVal()类似的操作。

看完了put()方法,下来我们看一下remove()方法:

// ① 根据key删除元素的方法
public V remove(Object key) {
    
    
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

@Override
// ① 根据key和value删除元素的方法
public boolean remove(Object key, Object value) {
    
    
    return removeNode(hash(key), key, value, true, true) != null;
}

/**
* Implements Map.remove and related methods
* 上述两个方法的具体实现都包含在此removeNode()方法中↓
* 这个方法的逻辑判断顺序和put()方法就有几分相似。
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
                          boolean matchValue, boolean movable) {
    
    
   Node<K,V>[] tab; Node<K,V> p; int n, index;
   // 首先还是判断当前数组不为空,接下来判断对应数组下标为hash值的元素是否存在
   if ((tab = table) != null && (n = tab.length) > 0 &&
       (p = tab[index = (n - 1) & hash]) != null) {
    
    
       Node<K,V> node = null, e; K k; V v;
       // 下来就是寻找对应remove的节点的逻辑步骤
       // 先检查当前数组下标位置的元素是否是要remove的元素
       if (p.hash == hash &&
           ((k = p.key) == key || (key != null && key.equals(k))))
           node = p;
       // 不是,就下一个
       else if ((e = p.next) != null) {
    
    
       	   // 下一个就不再那么单纯了,因为不清楚是树结构还是链表结构
       	   // 先判断是否为树结构,如果是,使用对应的树获得节点方法
           if (p instanceof TreeNode)
               node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
           else {
    
    
               // 否则,为链表结构,循环遍历
               do {
    
    
                   if (e.hash == hash &&
                       ((k = e.key) == key ||
                        (key != null && key.equals(k)))) {
    
    
                       node = e;
                       break;
                   }
                   p = e;
               } while ((e = e.next) != null);
           }
       }
       // 这里是根据matchValue和对应Value值是否相等来判断是否可以remove元素
       if (node != null && (!matchValue || (v = node.value) == value ||
                            (value != null && value.equals(v)))) {
    
    
           // 红黑树的删除节点方法,这里movable主要是树删除时需要使用的
           // 并且其中肯定包含untreeify()这个树转链表的方法
           if (node instanceof TreeNode)
               ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
           // 如果是数组上的节点
           else if (node == p)
               tab[index] = node.next;
           // 链表中除头节点以外的节点
           else
               p.next = node.next;
           ++modCount;
           --size;
           // 扩张方法
           afterNodeRemoval(node);
           return node;
       }
   }
   return null;
}

在JDK1.8的升级中,还添加了其他几个方法,这里我就不再展开了,附上几个我之前看过不错的博客,有兴趣的朋友可以了解一下:
3分钟了解 Map computeIfAbsent() 方法使用(有范例)
Java中映射Map的merge、compute、computeIfAbsent、computeIfPresent基本用法
————————————————————————————————————
OK,以上便是HashMap的全部内容了
我们ConcurrentHashMap再见。

猜你喜欢

转载自blog.csdn.net/cjl836735455/article/details/106970030