ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

HashMap原理分析及源码分析

2021-04-11 21:57:10  阅读:96  来源: 互联网

标签:分析 node hash HashMap 哈希 value 源码 key null


HashMap原理分析及源码分析

1. HashMap简介

  哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。
  在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能:

  1. 数组:
      采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n)。当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
  2. 线性链表:
      对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
  3. 二叉树:
      对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
  4. 哈希表:
      相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。

接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的:
  我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。比如我们要新增或查找某个元素,我们通过把当前元素的关键字,通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

  • 哈希冲突(哈希碰撞)
     如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表+红黑树的方式。

1.1 hashMap数据结构

1

  • 链表Node节点的定义:
 /** HashMap的节点类型。既是HashMap底层数组的组成元素,又是每个单向链表的组成元素 */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //key的哈希值
        final K key;
        V 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;
        }
    }

1.2. jdk 7 与 jdk 8 中关于HashMap的对比

1、JDK 8 之前:
 JDK 8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
 当 HashMap 中有大量的元素都存放到同一个桶(bucket)中时,这个桶下有一条长长的链表,极端情况HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
2、JDK 8:
 DK7与JDK8中HashMap实现的最大区别就是对于冲突的处理方法。JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn)),用数组+链表+红黑树的结构来优化这个问题。

  • 对比:
  • (1)JDK 8时数据结构是红黑树+链表+数组的形式,当桶内元素大于8时,便会树化
  • (2)hash值的计算方式不同,参考: Map中的hash()分析.
  • (3)JDK 1.7 table在创建hashmap时分配空间,而JDK 1.8在put的时候分配,如果table为空,则为table分配空间。
  • (4)在发生冲突,插入链表数据时,JDK1.7 是头插法,JDK1.8是尾插法。
  • (5)在resize扩容操作中,JDK1.7需要重新进行index的计算,而JDK1.8不需要,通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原index.

2. 源码分析

2.1 静态变量

1、table默认初始化容量 16。容量必须为2的次方。默认的hashmap大小为16.

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;

2、table最大的容量大小2^30,约10亿

static final int MAXIMUM_CAPACITY  = 1 << 30

3、默认负载因子大小为0.75,即实际数量超过总数DEFAULT_LOAD_FACTOR的数量即会发生resize动作。

static final float DEFAULT_LOAD_FACTOR = 0,75f: 

为什么是0.75,网上有些答案说是,因为capcity是2的次方,那么与之相乘会得到整数。还有一种说法更为可靠,负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
4、 触发树化的阈值1:将链表转化为红黑树的临界值。当添加一个元素被添加到有至少TREEIFY_THRESHOLD个节点的桶中,桶中链表将被转化为树形结构。 临界值最小为8。

static final int TREEIFY_THRESHOLD = 8;

5、触发树化的阈值2:桶可能被转化为树形结构的最小容量。当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突。

static final int  MIN_TREEIFY_CAPACITY = 64;
  • 触发树化的条件:单个链表的容量超过阈值8,且此时table的长度大于64

6、恢复成链式结构的桶大小临界值。当resize后或者删除操作后,单个链表的容量低于阈值时,将红黑树转化为链表。

static final int UNTREEIFY_THRESHOLD = 6;

2.2 成员变量

1、node数组

transient Node<K,V>[] table;

2、集合的键值对的个数

transient int size;

3、hashMap被结构化修改的次数,结构化修改指的是成功添加和删除数据引起的结构修改,排除覆盖和扩容(rehash)

transient int modCount;

4、扩容的阈值:hashMap的元素个数大于阈值时,需要进行扩容。
计算公式:负载因子容量
默认为:0.75
16 = 12

  int threshold;

5、负载因子:默认为0.75,一般不建议改

 final float loadFactor;

2.3 构造方法

1、无参构造

public HashMapp(){
	this.loadFactor = DEFAULT_LOAD_FACTOR;
}

2、指定初始容量和负载因子:

public HashMapp(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);
}

3、指定初始容量:

public HashMapp(int initialCapacity){
	this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
  • 一般用于确定map的元素个数时,减少扩容次数

2.4 get方法分析

/**返回指定的key映射的value,如果value为null,则返回null。
 * @see #put(Object, Object) */
public V get(Object key) {
    Node<K,V> e;
    //如果通过key获取到的node为null,则返回null,否则返回node的value。getNode方法的实现就在下面。
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

从源码中可以看到,get(E e)可以分为三个步骤:
1、通过hash(Object key)方法计算key的哈希值hash。
2、通过getNode( int hash, Object key)方法获取node。
3、如果node为null,返回null,否则返回node.value。

2.3.1 hash(Object key)方法

 HashMap的数据是存储在链表数组里面的。在对HashMap进行加、删除、查找等操作时,都需要根据K-V对的键值定位到他应该保存在数组的哪个下标中。而这个通过键值求取下标的操作就叫做哈希。
 求哈希简单的做法是先求取出键值的hashcode,然后在将hashcode得到的int值对数组长度进行取模。为了考虑性能,Java总采用按位与操作实现取模操作。
通过hash(Object key)方法计算key的哈希值hash,方法又可分为三步:
(1)取key的hashCode第二步
(2)key的hashCode高16位异或低16位,代码实现如下:

/**通过键的hashCode, 计算key的哈希值。 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(3)将第一步和第二部得到的结果进行取模运算,得到在哈希桶数组的位置。计算位置的方法如下:

//其中的n为数组的长度,hash为hash(key)计算得到的值
(n - 1) & hash
  • 用这个取余运算可以保证算出来的索引在数组大小范围内,不会超出。
  • Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

代码实现如下:

//根据hash值和数组长度算出索引值
static int indexFor(int h,int length){
	return h & (length-1);
}

2.3.2 getNode( int hash, Object key)

getNode方法又可分为以下几个步骤:
1、如果哈希表为空,或key对应的桶为空,返回null
2、如果桶中的第一个节点就和指定参数hash和key匹配上了,返回这个节点。
3、如果桶中的第一个节点没有匹配上,而且有后续节点:
(1)如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
(2)如果当前的桶不采用红黑树,即桶中节点结构为链式结构,遍历链表,直到key匹配
4、找到节点返回Node,否则返回null。

代码实现如下:

/**根据key的哈希值和key获取对应的节点
 * @param hash 指定参数key的哈希值
 * @param key 指定参数key
 * @return 返回node,如果没有则返回null
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //如果哈希表不为空,而且key对应的桶上不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //如果桶中的第一个节点就和指定参数hash和key匹配上了
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            //返回桶中的第一个节点
            return first;
        //如果桶中的第一个节点没有匹配上,而且有后续节点
        if ((e = first.next) != null) {
            //如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //如果当前的桶不采用红黑树,即桶中节点结构为链式结构
            do {
                //遍历链表,直到key匹配
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //如果哈希表为空,或者没有找到节点,返回null
    return null;
}

2.5 put方法分析

2.5.1 hashMap存储原理分析

举例说明:
2
通过图形表示为:
3
即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,如果超过,进行扩容。

2.5.2 put方法实现

/** 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
 * @param key 指定key
 * @param value 指定value
 * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
 */
public V put(K key, V value) {
    //putVal方法的实现就在下面
    return putVal(hash(key), key, value, false, true);
}

put(K key, V value)可以分为三个步骤:
 1、通过hash(Object key)方法计算key的哈希值。
 2、通过putVal(hash(key), key, value, false, true)方法实现功能。
 3、返回putVal方法返回的结果。
哈希值是如何计算的上面已经写了。下面看看putVal方法是如何实现的。

2.5.3 putVal方法

/**Map.put和其他相关方法的实现需要的方法
 * @param hash 指定参数key的哈希值
 * @param key 指定参数key
 * @param value 指定参数value
 * @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
 * @param evict 如果为false,数组table在创建模式中
 * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果哈希表为空,调用resize()创建一个哈希表,并用变量n记录哈希表长度
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果指定参数hash在表中没有对应的桶,即为没有碰撞
    if ((p = tab[i = (n - 1) & hash]) == null)
        //直接将键值对插入到map中即可
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //如果碰撞了,且桶中的第一个节点就匹配了
        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) {
                //如果到了链表尾部
                if ((e = p.next) == null) {
                    //在链表尾部插入键值对
                    p.next = newNode(hash, key, value, null);
                    //如果链的长度大于TREEIFY_THRESHOLD这个临界值,则把链变为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    //跳出循环
                    break;
                }
                //如果找到了重复的key,判断链表中结点的key值与插入的元素的key值是否相等,如果相等,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        //如果key映射的节点不为null
        if (e != null) { // existing mapping for key
            //记录节点的vlaue
            V oldValue = e.value;
            //如果onlyIfAbsent为false,或者oldValue为null
            if (!onlyIfAbsent || oldValue == null)
                //替换value
                e.value = value;
            //访问后回调
            afterNodeAccess(e);
            //返回节点的旧值
            return oldValue;
        }
    }
    //结构型修改次数+1
    ++modCount;
    //判断是否需要扩容
    if (++size > threshold)
        resize();
    //插入后回调
    afterNodeInsertion(evict);
    return null;
}

2.6 resize方法分析

1、为什么要进行resize扩容操作?
  向hashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,hashMap就需要扩大数组的长度,以便能装入更多的元素。当然数组是无法自动扩容的,扩容方法使用一个新的数组代替已有的容量小的数组。
  扩容的目的是减少hash碰撞,让key更加散列,从而提高hashmap的查询效率。
2、 在什么时候需要进行扩容?
  resize扩容操作主要用在两处:
(1)向一个空的HashMap中执行put操作时,会调用resize()进行初始化,要么默认初始化,capacity为16,要么根据传入的值进行初始化;

  • Map m = new HashMap(); 创建一个hashmap时,没有进行table数组的初始化,在第一次put操作时,进行table初始化。

(2)put操作后,检查到size已经超过threshold,那么便会执行resize,进行扩容,如果此时capcity已经大于了最大值,那么便把threshold置为int最大值,否则capcity,threshold进行扩容操作。

  • 发生了扩容操作,那么必须Map中的所有的数进行再散列,重新装入。
  • 扩容很耗性能。所以在使用HashMap的时候,先估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
  • resize方法非常巧妙,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。

具体扩容图如下:将一个原先capcity为16的扩容成32的:
6
在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变(因为任何数与0与都依旧是0),是1的话index变成“原索引+oldCap”。
例如:n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
7
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
8
可以将resize扩容的步骤总结为
1、计算扩容后的容量,临界值。
2、将hashMap的临界值修改为扩容后的临界值
3、根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
4、将旧数组的元素复制到table中。

/** 对table进行初始化或者扩容。
 * 如果table为null,则对table进行初始化
 * 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
 */
final Node<K,V>[] resize() {
    //新建oldTab数组保存扩容前的数组table
    Node<K,V>[] oldTab = table;
    //使用变量oldCap扩容前table的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //保存扩容前的临界值
    int oldThr = threshold;
    int newCap, newThr = 0;
    //如果扩容前的容量 > 0
    if (oldCap > 0) {
        //如果当前容量>=MAXIMUM_CAPACITY
        if (oldCap >= MAXIMUM_CAPACITY) {
            //扩容临界值提高到正无穷
            threshold = Integer.MAX_VALUE;
            //无法进行扩容,返回原来的数组
            return oldTab;
        }
        //如果现在容量的两倍小于MAXIMUM_CAPACITY且现在的容量大于DEFAULT_INITIAL_CAPACITY
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)
            //临界值变为原来的2倍
            newThr = oldThr << 1; 
    }//如果旧容量 <= 0,而且旧临界值 > 0
    else if (oldThr > 0) 
        //数组的新容量设置为老数组扩容的临界值
        newCap = oldThr;
    else {//如果旧容量 <= 0,且旧临界值 <= 0,新容量扩充为默认初始化容量,新临界值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {//在当上面的条件判断中,只有oldThr > 0成立时,newThr == 0
        //ft为临时临界值,下面会确定这个临界值是否合法,如果合法,那就是真正的临界值
        float ft = (float)newCap * loadFactor;
        //当新容量< MAXIMUM_CAPACITY且ft < (float)MAXIMUM_CAPACITY,新的临界值为ft,否则为Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //将扩容后hashMap的临界值设置为newThr
    threshold = newThr;
    //创建新的table,初始化容量为newCap
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //修改hashMap的table为新建的newTab
    table = newTab;
    //如果旧table不为空,将旧table中的元素复制到新的table中
    if (oldTab != null) {
        //遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果旧桶不为null,使用e记录旧桶
            if ((e = oldTab[j]) != null) {
                //将旧桶置为null
                oldTab[j] = null;
                //如果旧桶中只有一个node
                if (e.next == null)
                    //将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置
                    newTab[e.hash & (newCap - 1)] = e;
                //如果旧桶中的结构为红黑树
                else if (e instanceof TreeNode)
                    //将树中的node分离
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { //如果旧桶中的结构为链表。这段没有仔细研究
                    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;
}

2.7 remove方法分析

/**
 * 删除hashMap中key映射的node
 *
 * @param  key 参数key
 * @return 如果没有映射到node,返回null,否则返回对应的value。
 */
public V remove(Object key) {
    Node<K,V> e;
    //根据key来删除node。removeNode方法的具体实现在下面
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

从源码中可以看到,remove方法的实现可以分为三个步骤:
1、通过hash(Object key)方法计算key的哈希值。
2、通过removeNode方法实现功能。
3、返回被删除的node的value。
下面看看removeNode方法的具体实现,可以将removeNode方法的步骤总结为:
(1)如果数组table为空或key映射到的桶为空,返回null。
(2)如果key映射到的桶上第一个node的就是要删除的node,记录下来。
(3)如果桶内不止一个node,且桶内的结构为红黑树,记录key映射到的node。
(4)桶内的结构不为红黑树,那么桶内的结构就肯定为链表,遍历链表,找到key映射到的node,记录下来。
(5)如果被记录下来的node不为null,删除node,size-1被删除。
(6)返回被删除的node。
代码实现如下:

/** Map.remove和相关方法的实现需要的方法 -- 删除node
 * @param hash key的哈希值
 * @param key 参数key
 * @param value 如果matchValue为true,则value也作为确定被删除的node的条件之一,否则忽略
 * @param matchValue 如果为true,则value也作为确定被删除的node的条件之一
 * @param movable 如果为false,删除node时不会删除其他node
 * @return 返回被删除的node,如果没有node被删除,则返回null(针对红黑树的删除方法)
 */
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;
    //如果数组table不为空且key映射到的桶不为空
    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;
        //如果桶上第一个node的就是要删除的node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //记录桶上第一个node
            node = p;
        else if ((e = p.next) != null) {//如果桶内不止一个node
            if (p instanceof TreeNode)//如果桶内的结构为红黑树
                //记录key映射到的node
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {//如果桶内的结构为链表
                do {//遍历链表,找到key映射到的node
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        //记录key映射到的node
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //如果得到的node不为null且(matchValue为false||node.value和参数value匹配)
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            //如果桶内的结构为红黑树
            if (node instanceof TreeNode)
                //使用红黑树的删除方法删除node
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)//如果桶的第一个node的就是要删除的node
                //删除node
                tab[index] = node.next;
            else//如果桶内的结构为链表,使用链表删除元素的方式删除node
                p.next = node.next;
            //结构性修改次数+1
            ++modCount;
            //哈希表大小-1
            --size;
            afterNodeRemoval(node);
            //返回被删除的node
            return node;
        }
    }
    //如果数组table为空或key映射到的桶为空,返回null。
    return null;

原文参考链接:https://blog.csdn.net/zjxxyz123/article/details/81111627.
原文参考链接:https://blog.csdn.net/panweiwei1994/article/details/77244920.

标签:分析,node,hash,HashMap,哈希,value,源码,key,null
来源: https://blog.csdn.net/weixin_44462773/article/details/115600255

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有