集合源码阅读(一):基于jdk1.8的HashMap源码阅读分析

一、前言

在jdk1.8以前,HashMap采用数组+链表实现,采用拉链法来解决hash冲突,即创建一个链表数组,数组中每一格就是一个链表,遇到hash冲突直接将冲突的值塞进链表里即可,这样同一hash值的都存储在一个链表里。

这么做有个缺点就是如果同一hash值元素较多时,查找效率低下。在jdk1.8中为了解决hash冲突频繁的问题,HashMap采用了数组+链表+红黑树实现,当链表的长度超过阈值(8)时,将链表(查询复杂度ON)转换为红黑树(OlogN),极大地提升了查询效率。

jdk1.8还解决了多线程死循环的问题。不过HashMap仍然是非线程安全的,多线程可能会造成数据丢失问题。

二、对比jdk1.7与jdk1.8的HashMap

jdk1.7 jdk1.8以后
存储结构 数组+链表 数组 + 链表 + 红黑树
初始化方式 是一个单独函数:inflateTable() 直接集成到了扩容函数resize()中
hash值计算方式 9次hash方法处理:4次位运算 + 5次异或运算 2次hash方法处理:1次位运算 + 1次异或运算
存放数据规则 无冲突时,存放数组;冲突时,转换为链表 无冲突时,存放数组;冲突且链表长度 < 8时,存放单链表;冲突且链表长度 > 8:树化并存放红黑树
插入数据方式 头插法,先将原位置的数据移到后1位,再插入数据到该位置 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式 全部进行hash方法处理 按照扩容后的规律计算(扩容后的位置=原位置 or 原位置 + 旧容量)

三、源码阅读部分

1.声明

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable	
//Cloneable、Serializable为空接口,只是表明HashMap是支持克隆和序列化的

2.成员变量

//默认初始化数组容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	
//最大的数组容量,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认加载因子0.75,即一旦过了容量0.75倍就开始扩容
//使用0.75的原因是空间利用率比较高,避免了相当多的hash冲突,使得底层的链表或者红黑树的高度比较低,提升了空间效率
//如果使用1,则意味着只有table填满才进行扩容,这样就会有大量的hash冲突,导致底层红黑树变得十分复杂,降低了查询效率。就是牺牲时间来换空间
//如果使用0.5,则是table里元素到达一半就扩容,这样底层的链表长度或者红黑树高度就会减小,查询效率增加。可是这样空间利用率就降低了,牺牲空间换时间
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//由链表转红黑树的临界值
//至于为什么是8这个值,源码也说了,大概意思就是根据概率学泊松分布,当链表的长度等于8的概率已经特别特别小了,所以就以8为基准。
static final int TREEIFY_THRESHOLD = 8;

//由红黑树转链表的临界值
static final int UNTREEIFY_THRESHOLD = 6;

//table转换为树形结构的最小容量,如果table小于这个值,是不会出现红黑树的
static final int MIN_TREEIFY_CAPACITY = 64;

//表数据,即Node键值对数组,Node是单向链表,实现了Map.Entry接口,是2的幂次倍
transient Node<K,V>[] table;

//存放具体元素的集合
transient Set<Map.Entry<K,V>> entrySet;

//map中包含键值对的数量
transient int size;

//HashMap结构修改的次数
transient int modCount;

//Node数组的下一次扩容的临界值,第一次为16*0.75=12
//The next size value at which to resize (capacity * load factor) 0.75 * 最大容量 即阈值
int threshold;

//加载因子
final float loadFactor;

3.静态类

前面提到的Node<K, V>,是HashMap的内部类,实现了Map.Entry<K, V>接口。HashMap的table数组中存放的键值对对象就是Node<K, V>

static class Node<K,V> implements Map.Entry<K,V> {
    //hash值,根据该值确定记录位置
    final int hash;
    //node的key
    final K key;
    //node的value
    V value;
    //指向链表中的下一个元素,当链表中的元素超过TREEIFY_THRESHOLD(默认是8)时,将链表转换为红黑树。
    //此时该下标的元素将成为TreeNode<K, V>(红黑树结点)
    //该结点继承与LinkedHashMap.Entry<K, V>,而LinkedHashMap.Entry<K, V>又是Node<K, V>的子类。
    //综上HashMap底层数组元素的类型是Node<K, V>
    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;
    }
    //返回node里的键
    public final K getKey()        { return key; }
    //返回node里的值
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }
    
    //计算hashCode的方法,是key的hashCode与value的hashCode方法进行异或运算
    public final int hashCode() {
    	return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    
    public final V setValue(V newValue) {
    	V oldValue = value;
    	value = newValue;
    	return oldValue;
    }
    //判断两个entry是否相等,必须key和value相等才返回true
    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;
    }
}

前面提到的TreeNode<K, V> 的部分源码:

//红黑树的实现类,继承自LinkedHashMap.Entry<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;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    //构造函数
    TreeNode(int hash, K key, V val, Node<K,V> next) {
    	super(hash, key, val, next);
    }
    
    /**
     * 返回当前结点的根结点
     */
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
        	if ((p = r.parent) == null)
        		return r;
        	r = p;
        }
    }
}

4.核心方法

1.hash()算法:
//先取出对象的hashCode值,然后将hashCode值右移16位,然后将右移后的值与原来的hashCode进行异或运算,返回结果。
//如果key为null直接返回0
//通过key的hashCode的高16位异或或者低16位来实现的(h = key.hashCode()) ^ (h >>> 16)
//这么做的好处就是在table数组的length比较小的时候也能保证考虑到高低bit都参与到Hash的计算中,同时减少开销
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在这里讲一下计算下标,在putVal方法有这么一段代码:

if ((p = tab[i = (n - 1) & hash]) == null)  //获取位置
    tab[i] = newNode(hash, key, value, null);

tab就是table数组,n就是table数组的容量,始终为2的幂次,hash是上面方法的返回值。通过(n - 1) & hash来获取该对象的key在table的位置。为了效率,HashMap采用了(n - 1) & hash,它与hash % n等价,而与运算的效率是大于求模运算的。

计算下标的过程如下:

假设n是16
假入h = hashCode() :   1111 1111 1111 1111 1111 0000 1110 1010
即     h :             1111 1111 1111 1111 1111 0000 1110 1010
h >>> 16 :             0000 0000 0000 0000 1111 1111 1111 1111
hash = h ^ (h >>> 16) :1111 1111 1111 1111 0000 1111 0001 0101 

(n - 1) & hash   :     0000 0000 0000 0000 0000 0000 0000 1111
                        1111 1111 1111 1111 0000 1111 0001 0101
                        
                        0000 0000 0000 0000 0000 0000 0000 0101
得出下标为0101 也就是5

总结:

  • 1.HashMap采用了链地址法来链接拥有相同hash值的数据
  • 2.使用了2次hash函数来降低hash冲突的概率,使得数据分布的更平均

引申问题:为什么不直接使用hashCode处理后的值作为table下标?

答:这个问题其实挺简单的的,hashCode的取值范围是-2147483648到2147483647,约有40亿个映射空间,而HashMap则是默认16到2^30,HashMap一般是取不到那么大的,所以直接计算就会有很多问题。


2.将元素放进table里面
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
    	//判断table是否已经初始化
    	if (table == null) { // pre-size
    		//如果没有,s就为m的实际个数
    		float ft = ((float)s / loadFactor) + 1.0F;
    		int t = ((ft < (float)MAXIMUM_CAPACITY) ?
    				 (int)ft : MAXIMUM_CAPACITY);
    		//如果t大于阈值,则将阈值赋为大于t的最小二次幂
    		if (t > threshold)
    			threshold = tableSizeFor(t);
    	}
    	//已经初始化,并且s大于阈值,则进行扩容
    	else if (s > threshold)
    		resize();
    	//将map中的所有元素加入到table中
    	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);
    	}
    }
}

3.put方法,重点:
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

调用的putVal:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //步骤一:table未初始化或者长度0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
    //resize():扩容
    n = (tab = resize()).length;
    //步骤二:计算index,看是否是null
    //如果当前没有可以直接放,(n - 1) & hash 判断放在table的哪个桶中
    //当前新生成的结点实现放在数组中的
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    //桶中已经存在元素
    else {
        Node<K,V> e; K k;
        //步骤三:判断,新加进来的元素和之前存在的元素
        //如果两个key相等,新加进来的value直接覆盖旧的
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //将之前的元素赋给e,用e来记录
            e = p;
        //如果key不重复,判断是否得启动红黑树
        //如果是的话,则putTreeVal方法返回待存放的node,ps:e可能为null
        else if (p instanceof TreeNode)
            //将这个元素的信息放入红黑树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //这样就能加入进链表了
        else {
            //这个binCount用于判断当前的结点长度
            for (int binCount = 0; ; ++binCount) {
                //这个判断就是判断是否到了链表的最后一位
                if ((e = p.next) == null) {
                //在链表的最末尾插入结点
                p.next = newNode(hash, key, value, null);
                //判断是否到达了链表的临界值8
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    //将链表结构转换为树
                    treeifyBin(tab, hash);
                    break;
                }
                //判断链表中的key是否与新插入的相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    //如果相等直接跳循环
                    break;
                //用于遍历桶中的链表,与前面的e = p.next组合遍历链表
                p = e;
            }
        }
        //如果当前key已经存在,又来了一个相同的hash值和key值的,返回新来的
        if (e != null) { // existing mapping for key
            //保存旧的value
            V oldValue = e.value;
            //进行覆盖操作,条件是onlyIfAbsent为false或者oldValue为null
            if (!onlyIfAbsent || oldValue == null)
            	e.value = value;
            afterNodeAccess(e);
            //返回旧值
            return oldValue;
        }
    }
    //操作数加一,因为结构性修改了
    ++modCount;
    //如果加入元素后的容量大于阈值就进行扩容
    if (++size > threshold)
    resize();
    //回调函数
    afterNodeInsertion(evict);
    return null;
}

总结一下:

  • 1.在进行put操作的时候,首先判断键值对数组table长度是否为0或为null,如果是就进行第一次扩容
  • 2.根据key的hashCode与自身右移16位异或操作后在进行一波 (n - 1) & 这个值 来计算出要在table插入的位置
  • 3.如果当前位置没有元素,那就直接塞进table数组该位置即可,然后结束
  • 4.如果当前位置有元素,则进行一波判断操作:如果key一样,直接后面的覆盖掉前面的,如果key不一样的话就开始进行下面的判断
  • 5.判断这个位置是否已经红黑树了,如果是就直接给红黑树里面插结点
  • 6.如果不是红黑树,也就是链表,要进行尾插,先判断链表长度是否大于8,并且数组长度大于64,满足就转红黑树。如果在遍历链表的过程中发现了key相等的情况直接覆盖value
  • 7.插入成功后,判断实际存在的table数组的size是否超过了阈值threshold,如果超过进行扩容操作


4.resize() 扩容方法:
final Node<K,V>[] resize() {
    //申明一个oldTab指向table数组,进行操作
    Node<K,V>[] oldTab = table;
    //旧容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧的阈值
    int oldThr = threshold;
    //新容量和新的阈值
    int newCap, newThr = 0;
    //如果table数组不为空
    if (oldCap > 0) {
        //如果旧的容量已经大于最大HashMap容量1<<30,将阈值赋为2147483647
        //因为map是2的幂次方形式扩展的
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果当前table的长度在扩容后小于最大容量且最大容量大于16,新的阈值就是旧的阈值*2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //旧容量小于等于0,但是threshold大于零,代表是有参构造传入
    //threshold已经被初始化为最小2的n次幂,则直接将该值赋给新的容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //否则则是无参构造,则新的容量就是默认容量16,阈值就是16*0.75 = 12
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果新阈值为0,则表明是有参传入,且此时的旧阈值等于大于参数的最小二次幂
    //则让新阈值等于新的容量*0.75
    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"})
    //将当前的newTable 赋给 table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //如果原本的oldTable不是null开始执行下面
    if (oldTab != null) {
        //遍历旧数组的所有下标
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //将旧数组的所有元素赋给临时变量e,并且释放原数组中的引用null,这样可以保证数组被GC回收
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //如果e.next == null 表明table中就一个元素,不存在链表或者红黑树
                if (e.next == null)
                    //用hash映射算法把这个元素加入新的table里
                    newTab[e.hash & (newCap - 1)] = e;
                //如果e是红黑树的结点,并且e.next不是null,则得处理树的重排
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //否则e是链表的头且e.next不是null
                else { // preserve order
                    //loHead,loTail代表扩容后不用变换下标
                    Node<K,V> loHead = null, loTail = null;
                    //hiHead,hiTail代表扩容后要变换下标
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //遍历链表
                    do {
                        next = e.next;
                        //如果e的hash经过和旧用来与运算后 == 0,即都不一样,就是老值链表
                        if ((e.hash & oldCap) == 0) {
                        //初始化head指向链表的当前元素e,e不一定是链表的最后一个元素,
                        //loHead代表下标保持不变的链表的头元素
                            if (loTail == null)
                                loHead = e;
                            else
                                //如果loTail不是null,则loTail的下一个元素就是e,尾插法
                                loTail.next = e;
                            //让loTail指向当前元素e,使得lowHead可以连接到所有属于该链表的元素,避免了循环
                            loTail = e;
                        }
                        else {
                            //分析过程与前面差不多,新值链表
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //遍历结束,如果loTail.next不是null,则赋为null
                    //并把新的链表头放入新table的下标。
                    if (loTail != null) {
                        loTail.next = null;
                        //这是不改变下标的
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        //我们看出,改变下标的直接就是当前元素加上原本的容量
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

扩容方法小结:

  • 1.HashMap中的键值对大于阈值或者初始化的时候,调用此resize方法进行扩容
  • 2.每次扩展的时候,都是原来的二倍,初始化默认容量为16
  • 3.扩展后table中的node对象要么在原位置,要么移动到原偏移量两倍的位置。即如果当前是1,移动一次就是17
  • 4.在进行链表插入的时候,jdk7用的是头插法直接赋值,而jdk8用的是尾插法,先循环再赋值(do while)。这么做避免了链表带环死循环的问题

5.treeifyBin()方法,即树化方法:

链表长度binCount大于等于TREEIFY_THRESHOLD(8)且table容量大于64的时候,执行该方法就开始将链表转换为红黑树。这也就是说,不一定链表长度等于8就立刻扩容,而是先判断是否容量到达64。至于为什么是8这个值,参考源码,大概意思就是根据概率学泊松分布,当链表的长度等于8的概率已经特别特别小了(百万分之六),所以就以8为基准。树形化的时候,先去遍历链表中的所有元素,并创建对应的树结点,最后判断table的当前位置如果有树形结点,对其进行转换为红黑树的操作。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    ////如果table没有初始化,或者table长度小于MIN_TREEIFY_CAPACITY 64时,进行扩容而不是树形化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    //开始树形化,先判断是否该index有链表
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //hd:头结点,tl:尾结点
        TreeNode<K,V> hd = null, tl = null;
        //该循环是将原单向链表转换为双向链表
        do {
            //将每个链表结点转换为树的结点,其中链表的头结点就是树的头结点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            //如果尾结点为null则表明首次循环,此时e为头结点、p为根结点
            //因此将p赋给表示头结点的hd
            if (tl == null)
                hd = p;
            else {  //树的根结点已经产生了,则t1尾结点指向上次循环创建的树形结点。此时tl与p的关系是前后
                //产生当前结点与前驱结点之间的链
                p.prev = tl;
                //产生前驱结点和当前结点之间的链
                tl.next = p;
            }
            //将tl指向当前结点
            tl = p;
        } while ((e = e.next) != null);
        //如果当前table位置有树形结点,进行树形化
        if ((tab[index] = hd) != null)
            //根据链表创建红黑树结构
            hd.treeify(tab);
    }
}

6.get()方法,根据key取value

get方法执行的时候,首先进行判断:table已经初始化,table长度大于0,第一个元素非空。都符合则先看第一个元素是否符合要求(hash相等,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;
    //table已经初始化,长度大于0,且当前位置有元素。ps:直接通过hash就定位了table中的位置
    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;
}

7.remove()方法,根据key删除key-value对

HashMap中删除指定key对应的键值对,并返回被删除的键值对的值,如果返回空,说明key不存在或该key对应的值为null,如果想确认key是否存在使用containsKey方法。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

调用的removeNode方法:

/**
 * Implements Map.remove and related methods.
 *
 * @param hash 要删除的key的hash值
 * @param key 要删除的key值
 * @param value 要删除的key对应的value,该值作为删除的条件取决于matchValu是否为true
 * @param matchValue 如果true则key对应的值.equals(value)为true才删除,否则不关心value的值
 * @param movable 删除候是否移动结点,如果为false则不移动
 * @return 返回被删除的对象,没有删除任何结点返回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不为空,数组长度大于0,根据hash定位到当前结点不是null
    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;
        //如果当前结点就是我们要删除的(hash相等,且key相等),复制给node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        //否则表明,首结点并没有匹配到。如果p.next为空,直接返回null即可,就一个结点还匹配不到
        else if ((e = p.next) != null) {
            //判断是否为树结点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            //为链表结点
            else {
                do {
                    //如果找到了就把当前结点赋给node,然后break
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    //说明当前的e并不是我们要删除的,则让p存的是下一次循环里e的父结点。
                    //如果e匹配到了表明p是e的父结点
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //如果node不为空,说明匹配到了。
        //如果是直接不需要对比value的值,或者需要对比value的值但是value的值也相等
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            //如果是树形结点,调用树形结点的方法
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            //否则node为首结点,直接让该table的这个值等于node的下一位就行
            else if (node == p)
                tab[index] = node.next;
            //否则,node不是首结点,而是node的父结点,则链表删除
            else
                p.next = node.next;
            //操作数加一,元素个数减少
            ++modCount;
            --size;
            //无任何逻辑,就是让子类进行重写的
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

这篇文章是本人的一些拙见,如果有误,还请各位大牛在评论区指出,也欢迎大家一起交流,十分感谢!

猜你喜欢

转载自blog.csdn.net/laobanhuanghe/article/details/105662262