HashMap源码解析JDK1.8 —————— 开开开山怪

hashMap源码解析

导航

1.hashMap的数据结构
2.影响hashmap性能因素
  • key的hash实现
  • loadFactor的设定
  • 数组capacity的设定
3.hashmap的主要方法解析
  • put(K key, V value)
  • get(Object key)
  • resize()
  • remove(Object key)
  • containsKey(Object key)

正题

1.hashMap数据结构

hashMap的主要数据结构为 数组 + 链表(红黑树)

HashMap源码解析JDK1.8 —————— 开开开山怪_第1张图片
链表节点

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; //键的hash值,通过hash方法计算得到
    final K key;  //键本身
    V value;      //值本身
    Node<K,V> next;  //当前node的后驱节点引用
 }

红黑树节点

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // 当前节点的父节点
    TreeNode<K,V> left;    //当前节点的左孩子
    TreeNode<K,V> right;   //当前节点的右孩子
    TreeNode<K,V> prev;    //当前节点的前驱节点
    boolean red;  //当前节点的颜色
 }

当数组中的每个bin中的节点构成的链表到达一定的长度,链表会转化为红黑树,但是在构成红黑树的同时不会破坏之前的链表的链接关系,只会将红黑树中的根节点在原链表中的位置和原链表的头节点进行替换。(后边会有详细的图解表明不会破坏之前的链表的关系,只是新增添了成员进行红黑树的转化)

2.影响hashMap的性能因素

hashMap成员:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // hashMap默认的数组容量为16
static final int MAXIMUM_CAPACITY = 1 << 30;//hashMap默认的数组最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子的值
static final int TREEIFY_THRESHOLD = 8;//每个bin中的链转化为红黑树的节点个数阀值
static final int UNTREEIFY_THRESHOLD = 6;//每个bin中从红黑树转化为链表的节点个数的阀值
static final int MIN_TREEIFY_CAPACITY = 64;//将bin中链表进行红黑树转化的数组最小容量,具体的意思就是,如果数组的容量<64,那么当bin中的链表节点个数超过8个,也不会进行红黑树的转化,而是采取扩容的手段。

transient Node<K,V>[] table;  //主要存储结构,node数组
transient Set<Map.Entry<K,V>> entrySet;//
transient int size;//放入到数组中的元素的个数
transient int modCount;//容器的被修改次数,增删改都会对此变量加1
int threshold;//此变量是需要扩容的阀值,采用capacity*loadFactor得出

final float loadFactor;//负载因子,当数组中的元素的个数大于capacite * loadFactor是需要进行数组的扩容,0.75为默认值。

1.loadFactor的设定

1. loadFactor:负载因子对于hashmap来说是其性能的关键因素之一
当loadFactor较小时,浪费的table空间较大,扩容较频繁,size的个数很快达到扩容的阀值。但好处是减小了冲突 (冲突是指不同的节点被放入同一个bin中的可能性),相当于每个bin中差不多只有一个节点,put操作和get操作代价较低,相当于是O(1);
2. 当loadFactor较大时 (loadFactor的值可大于1) ,这时候空间利用率较高,size的个数不会很快达到阀值,但同时带来的问题就是冲突增大,因为可容纳节点越多,被放入同一bin的可能性越大,而对之后的put和get的操作,可能就要进行遍历链表或红黑树,相对费时。

所以结合时间复杂度和空间利用率来看,0.75在这两反面来说相对均衡。

2.key的hash实现

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里采用的key.hashCode右移16为再与其进行异或计算出最后的key的hash值,再采用这个hash进行与数组容量 - 1进行相与计算出节点应放入哪个下标的bin中。

原因: 我们在进行存储节点的时候采用的是key.hash &(capacity -1),就是根据key的hash值与数组的容量进行取余(hashMap采用位运算的方式key.hash &(capacity -1),运算速度更快),让节点随机落在数组的某个bin中,hash值相对数组的容量值来说大得多,所以进行位运算时,只有低位能用的上,而hash的高位的随机性会产生浪费,所以需要采用上边代码的方式,目的就是增加hash值的随机性,落入bin的随机性,进而减少冲突,提高hashMap的性能。

3.数组capacity的设定

补充:为什么table数组的容量要求为2的整数次幂
hashMap中采用这个方法保证,当用户给定一个容量的时候,能计算出比给定容量大的最小的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;
}

此方法,用来计算应该申请多大的数组,如果用户不设置size的话,则默认的size为16,
如果用户设置size的话,则最后的数组size大小,通过这个方法计算出一个大于用户给定size的最小数,这个数为2的整数次方
例如cap如果是5, 则n = 5 -1 = 0100
n |= n >>> 1 = 0110
n |= n >>>2 = 0111
n |= n >>> 4 = 0111
n |= n >>> 8= 0111
n |= n >>> 16 = 0111
return 0111 + 1 = 1000;

如果cap = 6 则n= 6-1 = 0101
n|= n>>>1 = 0111;
n|= n>>> 2 = 0111;
n |= n >>> 4 = 0111
n |= n >>> 8= 0111
n |= n >>> 16 = 0111
return 0111 + 1 = 1000

最后返回中的值都是比capacity大的最小的2 的整数次方
目的:同样也是为了减少节点落入bin中的冲突 ,因为决定节点落在哪个bin采用的是key.hash &(capacity - 1)计算出数组下标进行放入;那当capacity为2的整数次幂的话,capacity - 1的值采用二进制表示的话,除过最高位为0,后边的值都为1,那么此时随机性完全取决于hash的随机性,如果capacity采用的是奇数那么此时capacity-1就为偶数,那么capacity-1的值采用二进制表示的话有多位都是0的情况,那么再与hash进行相与的时候,完全抹杀了hash的低位某些位的随机性,不管hash值的低位的某些位是1还是0,最后相与0相与的结果都是零。同样这也就是hashMap为什么要求table的容量为2的整数次幂。

3. hashmap的主要方法解析

1.put(K key, V value)

put(K key, V value)->putVal(hash(key), key, value, false, true)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

putVal方法

流程图
HashMap源码解析JDK1.8 —————— 开开开山怪_第2张图片

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//这个针对进行完初始化,首次进行put的时候,需要进行扩容操作。
    if ((p = tab[i = (n - 1) & hash]) == null)
        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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) //流程图这部分没有画,
          //这部分就是,当将新节点链入到链表的末尾时,
        //此时节点个数大于到达转化为红黑树的节点数阀值时 treeifyBin(tab, hash)方法先判断table的容量是否大于链表转化为红黑树的最小table容量即MIN_TREEIFY_CAPACITY = 64,这个值在最开始已经解释过了
        //如果不满足,那只进行扩容操作,如果满足,先组成链表的普通节点先转化红黑树节点,在将链表转化为红黑树的形式,
       //红黑树节点是在node节点上进一步增加了成员,在转化为红黑树的时候,之前node节所保持的链表采用next来链接的这种关系并未有抹杀,只是又增添了每个节点的父节点,左孩子节点,右孩子节点和前驱节点,以及该节点的颜色仅此而已
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //此处表明放入的键与原map中的键相等,只需更改值
        //onlyIfAbsent默认值是false,表示允许更改值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);//这个方法再hashmap中并没有实现,但是在linkedHashMap中有实现,因为当linkedhashMap中将e!= Null看做是对map中的键值进行访问,相当于想要put的键map中已经存在,当做是访问看待,afterNodeAccess方法会将当前访问的这个节点移至链表的末尾,这里说的链表是在linkedhashMap中做维持的链表。
            return oldValue;
        }
    }
    //每次的put操作相当于修改了hashmap,修改此时加1.
    ++modCount;
    if (++size > threshold)//当map中的节点个数大于扩容阀值,进行扩容操作。
        resize();
    afterNodeInsertion(evict);
    return null;
}

2.get(Object key)

get方法返回的值为null有两种情况
1.getNode(hash(key), key)的返回值为null,表明在map中没有该键,此时get方法返回null
2.getNode(hash(key), key)的返回值不为null,get的返回值为e.vaue.由于map中国允许键值都可以为null。所以也有可能存在该键,但值为null。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //首先table不为null,并且table的长度大于零,并且根据需要查找的key的hash值来确定节点应在table中的哪bin中,并且bin中拥有节点,才有继续查询的必要。
 @1.   if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //代码能到这里说明key的hash值经过计算做确定的bin中有节点
        //先判断第一个节点是否匹配
 @2      if (first.hash == hash && //第一个节点就相等直接返回该节点
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
          //代码到这里说明bin中有第一个节点,并且第一个节点不是匹配节点
          //先判断该节点是否有后驱节点
          //如果没有后驱节点,那么可以断定要查找的节点不存在于map中
 @3       if ((e = first.next) != null) {
           //代码到这里说明有后驱节点
           //然后进行链表和红黑树节点的判断,在根据不同情况进行处理
            if (first instanceof TreeNode)
            //如果bin中的数据结果属于红黑树,根据红黑树的查询方式进行查找,并返回
                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);
        }
    }
    //当map中不存在所要查找的键时候,返回null。
    return null;
}

在一开始展示了hashMap的基本结构,最开始的table数组的bin中的结构只是为了大家更清楚的知道,每个bin中可能存在链表,也可能是红黑树的结构,下面这个图更直观,(同样也解释了我之前说的,转变为红黑树结构后,并没有破坏链表的关系。)
HashMap源码解析JDK1.8 —————— 开开开山怪_第3张图片

3.resize()

对于hasnmap来讲,和ArrayList一样,也采用的是懒加载的模式,在进行初始化的时候并没有初始化table,只是简单的对负载因子,和扩容阀值进行设定,并且在初始化的时候如果采用的是带容量参数的构造方法进行初始化,扩容阀值=capacity,而不是capacity * loadFactor;真正的capacity * loadFactor的设定是在经构造方法初始化后,第一次进行put操作调用resize方法进行的。

resize 真正的用途有两处,同样也是resize调用的两种情况。
情况1 : 在进行初始化之后,第一次进行put操作,再对扩容阀值进行设定,并在此申请该有的数组空间。
情况2 : 当数组的元素个数大于扩容阀值,也是真正的由于数组容量不够进行扩容的情况。
由于 resize 代码太长,我拆分成两部分来说,可以对照下面源码进行理解

第一部分:扩容容量与扩容阀值的确定

第一部分的内容的目的就是计算出新的容量值,和新的扩容阀值,并且进行数组的申请。

1.resize调用情况一

1 :采用的是有initialCapacity参数的构造方法进行初始化之后

 int oldCap = 0;
 int oldThr = threshold = tableSizeFor(initialCapacity);
 int newCap, newThr = 0;
 不满足 if (oldCap > 0),满足else if (oldThr > 0),
 newCap = oldThr = tableSizeFor(initialCapacity);
 此时满足if (newThr == 0) 
 newThr = newCap * loadFactor;将扩容阀值重新进行设定。

2采用的是无initialCapacity参数的构造方法进行初始化之后

 int oldCap = 0;
 int oldThr = threshold = 0;
 int newCap, newThr = 0;
 不满足 if (oldCap > 0),不满足else if (oldThr > 0),满足最后一个条件
 newCap = DEFAULT_INITIAL_CAPACITY = 16;
 newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY) = 16 * 0.75;
2.resize调用情况二

int oldCap = oldTab.length ;
int oldThr = threshold = 旧的capacity * loadFactor;
int newCap, newThr = 0;
满足if (oldCap > 0)

1. 如果满足if (oldCap >= MAXIMUM_CAPACITY) ,因为容量已经够大,直接将扩容阀值改为Integer.MAX_VALUE,不用再进行扩容,直接往table中添加节点。

2. 如果不大于hashMap默认数组的最大容量,进入 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
满足新的容量不大于最大容量,旧容量必须大于hashmap默认的数组容量。
此时对于采取的是二倍扩容法,新的容量为旧容量的二倍,并且扩容阀值也不采用负载因子进行计算,同样新的扩容阀值为旧的扩容阀值的两倍。
newCap = oldCap << 1;
newThr = oldThr << 1;

3. 如果上述条件不满足,说明进行二倍扩容后,容量大于hashmap默认的最大容量,或者旧的容量小于默认数组容量,则分为下面两种情况

@1. 表明扩容后容量大于hashmap默认数组最大容量
newCap = oldCap << 1;
newThr = Integer.MAX_VALUE;

@2. 表明扩容后不大于hashmap默认的数组最大容量,但是旧的容量小于数组默认容量16;
newCap = oldCap << 1
newThr = newCap * loadFactor;

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //先将table的 旧容量 和 旧阀值 记录。
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
     int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {//程序进到这个判断,说明是真的需要扩容,而不是在进行初始化之后的第一put时候进行数组的初始化。
        if (oldCap >= MAXIMUM_CAPACITY) {
        //当旧容量大于默认的table数组最大容量时,
        //直接让扩容阀值 = Integer.MAX_VALUE;
        //因为此时的容量早已超过MIN_TREEIFY_CAPACITY
        //
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //当旧容量不大于默认的数组最大容量,我们采用的是2倍扩容法
        //新的容量为旧容量的二倍,至于为什么是2倍,resize代码的第二部分会进行详细说明
        //newCap = oldCap << 1;
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    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;
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];   
    table = newTab;

此上的将resize调用后,新的数组容量值和扩容阀值的情况分析完毕。
下面则根据新的数组进行扩容后的操作(进行原数组中节点的挪动操作)

第二部分:链的拆分过程(解释为什么扩容为原数组容量的2倍)
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;
        if ((e = oldTab[j]) != null) {//下标为0的元素,判断下标为0是否有元素存在,如果存在,进入到下面,如果不存在则进入下一次循环,目的是找到数组的第一个有效节点开始进行遍历。
        //进入if表明当前数组下标中有元素
            oldTab[j] = null;//先令当前的下标中值为null,为之后会将当前下标中的链拆分的链重新放入该下标中做准备。
            if (e.next == null)//如果当前的下标中有元素,并且表明该元素没有后驱节点,则直接将该元素的hash和newCap - 1进行相与得到新数组的对应下标。
                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 {//这一层else是在取出数组第一个节点,此时第一个节点不为null,并且第一个节点还有后驱节点的情况
                   //我们需要将该节点所引的链中的节点部分挪到新的数组中
                    next = e.next;//这个e代表的是从第一个有效节点的bin中取到的第一个元素,程序能到这里来说明此bin中是个链
                    //先保存第一个节点的后驱节点,next为下一个节点也即后驱节点

//下面if判断就是为了将原数组中某一下标的链中拆分为两个链,分别放入到新数组中下标为原数组的被拆分链所在的下标,或者为原数组被拆分链所在下标 + n。
//具体分析:
//本身我们通过e.hash &(n - 1)来确定将节点放在数组的那个坐标下,因为现在扩容的原因
//因为在扩容的过程中,采用二倍的方法进行扩容,
//举个栗子如果原来的数组的容量为16,那么节点所在下标为 hash & (16 -1)也相当于hash & 0000 1111
//当进行扩容的时候,容量扩大为2倍为32,那么之前在原数组中的节点在新数组的下标为 hash &(32 -1)相当于 hash & 0001 111
// 此时hash &(0001 1111)= hash &(0000 1111 | 0001 0000)=hash &(0000 1111)| hash &(0001 0000)
//hash &(0000 1111) 为原来数组中的下标
//hash &(0001 0000)为原来的容量n,拿现在的列子来说就是n = 16,
//对于扩容后对于原来数组中某一下标中元素在新数组中的下标就是:  原数组下标 + hash &(0001 0000)
//而对于 hash & (0001 0000)的结果只有两个,一个是0000 0000  一个是 0001 0000,总结来说,要不然是0 要不然就是n
//所以原来数组中的某一元素转移到新的数组中的下标 ,要么是原下标,要么是原下标 + n。此时的n就是capacity。

                    if ((e.hash & oldCap) == 0) {//当前元素的hash值与n相与,目的为了判断该元素在新数组的位置到底是和原来下标相同是原来下标+n,
          //进入当前if判断表明(e.hash & oldCap)==0,表明当前正在遍历的节点在放入新数组的位置和原数组的位置相同(即下标相同的bin中)
        //下面的操作就是将本次遍历的原数组bin中的链表的部分节点构成一个新的链
                        if (loTail == null)//因为在一开始的时候新数组中没有任何的元素,所以末节点为null
                            loHead = e;//此时将满足下标的第一个元素设置为头节点
                        else
                            loTail.next = e;//程序到这一步,说明已经有第二个满足该下标的节点,将该节点连在末节点后边
                        loTail = e;//每一轮的当前元素都是e,
 //这里用了个技巧,分为第一次情况和之后几次的情况,对于第一个情况来说,只设置并保存头节点
//对于之后的来说,只是将节点当前节点连接在末节点之后。
                    }
                    else {
 //这里的else情况表明(e.hash & oldCap)!= 0,表明当前正在遍历的节点在放入新数组的位置和原数组位置下标 + n的值相同(也即原数组下标 + capcaity 下标位置)
 //下面的操作就是将本次遍历的原数组bin中的节点满足应落在新数组中 原数组下标 + capcaity下标位置 的节点组成另一条链。

                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null);
      //下面的两个if操作就是将上面分别形成的两条链放入到新数组不同的bin中
      //newTab[j] 表示与原数组下标相同的bin
      //newTab[j + oldCap] 表示与原数组下标 + n 相同的bin
      
               
                if (loTail != null) {//因为lotail代表的是当前e,
                    loTail.next = null;//让末节点的后驱节点设置为null
                    newTab[j] = loHead;//并且将新链表的头节点放置在新数组的对应下标(该链在原数组中的下标)中,
                }
                if (hiTail != null) {//而这个则是在新数组中下标为原数组下标 + n处的下标放置原数组下标的链的部分节点
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
}
return newTab;

4.remove(Object key)

一般容器的删除方法都会将删除的元素进行返回,但remove的返回值null同样有两种情况
1.没有找到要删除的键,返回为null
2.找到了要删除的键,返回该键的对应值,值可能为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;
    //通过对象查找对象进行删除,首先确保想要删除的键在hashMap中存在,
    //通过键的hash中来判断数组中通过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;
        //表明下标中拥有节点,先进行第一个节点的判断,、
        //若第一个节点是要删除的节点,进行入if
        //若第一个节点不是要删除的节点,进入else if
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //第一个节点就是要删除的节点,用node保存该节点
            node = p;
        else if ((e = p.next) != null) { //表明下标中第一个节点不是要删除的节点,继续判断,第一个节点是否有后驱节点
        //没有后驱节点 :直接跳出当期那else if,表明hashmap中没有对应节点要删除     
        //有后驱节点,才进入下面的if 和else的判断
       
            if (p instanceof TreeNode)//当前节点为红黑树节点,根据红黑树的查找删除规则进行,node 的值可能为null,可能为查找到的节点。
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {//当前是链表,根据链表的规则进行查找删除
            //node有两个是1.null 2.非null
            //node值为null说明没有要删除的节点,不为null说明存要删除的节点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //根据node的值来进行下一步操作
        //node为null,表明没找到要删除的键,不进入下面的if,直接返回null
        //node不为null,进入到下面的if,因为remove(Object)方法默认的matchValue 为false.如果是remove(Object, Object),matchValue 默认为true,,所以还需要根据值是否相等进行删除。
        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)//这里表示的意思是下标的中的第一个节点就是要删除节点,直接让该下标的节点为删除节点的后驱节点。
                tab[index] = node.next;
            else//保存的节点为普通节点,根据链表的形式进行删除
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);//这个方法同样哎hashMap中没有实现
            //但是在linkedHashMap中有所实现。
            return node;
        }
    }
    return null;
}

5.ContainsKey(Object key)

这个方法事实上内部调用了getNode方法,getNode方法再上边已经有所讲述当hashMap中没有这个相同的key是返回值为null,ContainsKey通过返回的是否为null来判断是否拥有键。

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

你可能感兴趣的:(java源码,java)