HashMap源码详解

HashMap是基于哈希表的 Map 接口实现。此实现提供所有可选的映射操作,并允许 null 值和 null 键。(该 HashMap 类大致等价于 Hashtable,只是它是非同步的,允许 null。此类不保证地图的顺序;特别是它不能保证订单会随着时间的推移保持不变。)
此实现为基本操作 (get 和 put) 提供恒定时间性能,假设哈希函数将元素正确分散在存储桶之间。对集合视图进行迭代所需的时间与实例的 HashMap “容量”(存储桶数)及其大小(键值映射数)成正比。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载系数太低),这一点非常重要。
实例 HashMap 有两个影响其性能的参数: 初始容量 和 负载系数。 容量:是哈希表中的存储桶数,初始容量只是创建哈希表时的容量。 负载系数:是衡量哈希表在自动增加容量之前允许达到的满度的度量。当哈希表中的条目数超过负载系数和当前容量的乘积时,将重新散列哈希表(即重新构建内部数据结构)使哈希表的桶数大约是其两倍
作为一般规则,默认负载系数 (.75) 在时间和空间成本之间提供了良好的权衡。较高的值会减少空间开销,但会增加查找成本(反映在类的大多数 HashMap 操作中,包括 get 和 put)。在设置映射的初始容量时,应考虑映射中的预期条目数及其负载系数,以最大程度地减少重新散列操作的次数。如果初始容量大于最大条目数除以负载系数,则不会发生重新哈希操作。
如果要在实例中 HashMap 存储许多映射,则创建具有足够大容量的映射将允许更有效地存储映射,而不是让它根据需要执行自动重新哈希以增加表。请注意,使用多个相同的 hashCode() 键肯定会降低任何哈希表的性能。为了改善影响,当键是 Comparable 时,此类可以使用键之间的比较顺序来帮助打破联系。
请注意,此实现不会同步。 如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。这通常是通过在自然封装地图的某些对象上进行同步来实现的。如果不存在此类对象,则应使用该 Collections.synchronizedMap 方法对地图进行“包装”。最好在创建时执行此操作,以防止意外不同步访问地图:Map m = Collections.synchronizedMap(new HashMap(...));
由此类的所有“集合视图方法”返回的迭代器都是 快速失败的:如果在创建迭代器后的任何时间对映射进行结构修改,则迭代器将 remove 抛出一个 ConcurrentModificationException。因此,在面对并发修改时,迭代器会快速而干净地失败,而不是在未来不确定的时间冒着任意的、非确定性行为的风险。
请注意,迭代器的快速失效行为是无法保证的,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬性保证。快速失败的迭代器在尽力而为的基础上抛出 ConcurrentModificationException 。因此,编写一个依赖于此异常的程序是错误的: 迭代器的快速失败行为应该仅用于检测错误。

一、HashMap实现结构

1、从结构实现来讲,HashMap是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的

2、HashMap中的重要参数


  //threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
  int threshold;             

  //默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
  final float loadFactor;


  //modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。
  transient int modCount;  


  //HashMap中实际存在的键值对数量。注意和table的长度length、容纳最大键值对数量threshold的区别。
  transient int size;  

以下是默认值: 

二、哈希桶数组(Node)

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。

HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //定位数组索引的位置
        final K key;
        V value;
        Node<K,V> next;   //链表的下一个node

        Node(int hash, K key, V value, Node<K,V> next) { ... }
        public final K getKey(){ ... }
        public final V getValue() { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
}

二、get过程

    /**
     * 获取指定键所映射的值。
     * 
     * 如果此映射包含从键到值的映射,则返回该值;如果不存在这样的映射,则返回null。
     * 更正式地说,如果此映射包含从键 {@code k} 映射到值 {@code v} 的映射,并满足
     * {@code (key==null ? k==null : key.equals(k))},则此方法返回 {@code v};否则返回 {@code null}。
     * (最多存在一个这样的映射。)
     * 
     * 返回值为 {@code null} 不一定意味着该映射不包含此键的映射;也有可能是该映射明确将此键映射到 {@code null}。
     * 可以使用 {@link #containsKey containsKey} 方法来区分这两种情况。
     * 
     * @param key 用于获取其值的键。
     * @return 与指定键相映射的值,如果没有映射则返回null。
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        // 尝试获取与给定键相匹配的节点,如果找不到则返回null
        return (e = getNode(key)) == null ? null : e.value;
    }

    /**
     * 实现Map.get方法及其相关方法。
     * 
     * @param key 用于查找的键
     * @return 找到的节点,如果没有找到则返回null
     */
    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        // 如果表不为空且表长度大于0,以及根据键计算出的索引处的节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            // 如果首个节点的hash值与键相等,并且键相等或能通过equals方法判断相等,则返回该节点
            if (first.hash == hash && // 总是检查首个节点
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 首个节点之后存在节点
            if ((e = first.next) != null) {
                // 如果首个节点是TreeNode类型,则使用特定的TreeNode方法进行查找
                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);
            }
        }
        // 如果没有找到匹配的节点,返回null
        return null;
    }

三、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,如果超过,进行扩容。

JDK17中PUT过程的源码:

    /**
     * 将指定的值与指定的键在这个映射中关联起来。如果这个映射之前包含了一个与该键相关的映射,
     * 则旧的值会被替换。
     *
     * @param key 与指定值相关联的键。
     * @param value 要与指定键关联的值。
     * @return 与 {@code key} 相关联的先前值,如果没有映射与 {@code key} 相关联,则返回 {@code null}。
     *         (返回 {@code null} 也可以表示该映射之前将 {@code null} 与 {@code key} 相关联。)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 插入一个键值对到哈希表中。如果key已经存在,根据onlyIfAbsent参数决定是否更新value;
     * 如果evict为true,则在插入后可能触发淘汰操作。
     * 
     * @param hash 键的哈希值
     * @param key 要插入的键
     * @param value 要插入的值
     * @param onlyIfAbsent 如果为true,且key已存在,则不更新value
     * @param evict 如果为true,插入后可能触发淘汰操作
     * @return 如果存在旧的键值映射,返回旧的值;否则返回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;
        // ①:tab为空则创建,也就是当哈希表为空或长度为0时,进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // ②:计算index,尝试在哈希表中插入新的键值对,并对null做处理
        // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 处理哈希冲突的情况
            Node<K,V> e; K k;
            // ③:节点key存在,直接覆盖value
            // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
            // ④:判断该链为红黑树
            // hash值不相等,即key不相等;为红黑树结点
            // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
            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);
                        // 当链表长度超过阈值8时,将链表转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //链表结构转树形结构
                            treeifyBin(tab, hash);
                        // 跳出循环
                        break;
                    }
                    // key已经存在直接覆盖value
                    // 判断链表中结点的key值与插入的元素的key值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 相等,跳出循环
                        break;
                    // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                    p = e;
                }
            }
            // 处理插入或更新操作
            if (e != null) { // existing mapping for key
                // 记录e的value
                V oldValue = e.value;
                // onlyIfAbsent为false或者旧值为null
                if (!onlyIfAbsent || oldValue == null)
                    //用新值替换旧值
                    e.value = value;
                // 访问后回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // ⑥:超过最大容量就进行扩容
        // 结构性修改
        ++modCount;
        // 实际大小大于阈值则扩容
        if (++size > threshold)
            resize();
        // 插入后回调
        afterNodeInsertion(evict);
        return null;
    }

四、扩容机制(resize)

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

①、判断当前容量大小,大于0且没有达到最大值时进行扩容,默认2倍,

②、根据新容量和加载因子计算表的新阈值

③、创建新表,假设旧表不为空,就把旧元素迁移到新表当中,如果新位置已经有元素,则将旧元素添加到新位置元素的链表头部(对于链表结构)或合并到对应的红黑树(对于红黑树结构)。在迁移过程中,如果原数组中的元素本身是一个链表(或红黑树),则整个链表(或红黑树)会被整体迁移至新数组的相应位置,保持原有顺序不变。

    /**
     * 调整哈希表的大小,当表满或超过预设阈值时,会双倍扩容。此方法会重新分配元素到新的表中,
     * 保证哈希表的正常功能。
     *
     * @return 新的哈希表数组
     */
    final Node<K,V>[] resize() {
        // 获取当前表和阈值
        Node<K,V>[] oldTab = table; //oldTab指向hash桶数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        
        // 如果当前容量大于0
        if (oldCap > 0) { //如果oldCap不为空的话,就是hash桶数组不为空
            // 如果容量已达到最大值,则不再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果大于最大容量了,就赋值为整数最大的阀值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 当容量小于最大值且大于默认初始容量时,阈值翻倍
        }
        // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
        else if (oldThr > 0) // 如果初始化阈值已设置,则使用该值作为新容量
            // 直接将该值赋给新的容量
            newCap = oldThr;
        else {               // 如果初始化阈值为0,使用默认设置
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        
        // 根据新容量和加载因子计算新阈值
        if (newThr == 0) {
            // 新的threshold = 新的cap * 0.75
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr; // 更新阈值
        
        // 创建新表
        @SuppressWarnings({"rawtypes","unchecked"})
        //新建hash桶数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab; // 更新表引用
        
        // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使 其均匀的分散
        // 如果旧表非空,则将元素迁移到新表中
        if (oldTab != null) {
            // 遍历新数组的所有桶下标
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
                    oldTab[j] = null; // 清空旧表引用,防止循环引用
                    // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
                    if (e.next == null)
                        // 用同样的hash映射算法把该元素加入新的数组
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
                    else if (e instanceof TreeNode) // 如果是红黑树节点,进行树拆分
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                        // 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;
                            // 根据旧容量判断节点应放置在新表的哪个位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
                                    // 代表下标保持不变的链表的头元素
                                    loHead = e;
                                else
                                    // loTail.next指向当前e
                                    loTail.next = e;
                                // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
                                // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素
                                loTail = e;
                            }
                            // 原索引+oldCap
                            else {
                                if (hiTail == null)
                                    // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 将拆分后的节点链重新放入新表中
                        // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab; // 返回新表
    }

五、ConcurrentHashMap

ConcurrentHashMap 在 JDK1.8 使用了 CAS 乐观锁 + synchronized 局部锁 处理并发问题,锁的粒度更细,即使数据量很大也能保证良好的并发性。

1、put源码

    /**
     * 插入键值对到容器中。如果键值对不存在且onlyIfAbsent参数为false,则插入该键值对。
     * 
     * @param key  要插入的键,不能为空。
     * @param value  要插入的值,不能为空。
     * @param onlyIfAbsent  如果为true,则仅在键已存在时才不插入新键值对。
     * @return 如果插入了新键值对,则返回null;如果键已存在且onlyIfAbsent为true,则返回该键对应的旧值。
     */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // 检查key和value是否为null
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());  // 计算key的哈希值
        int binCount = 0;  // 用于统计链表或树中节点的数量
        // 循环开始,尝试插入键值对
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 如果table为空,则初始化table
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 尝试在空的bucket中添加新节点
                // 注意!这是一个CAS的方法,将新节点放入指定位置,不用加锁阻塞线程也能保证并发安全
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;
            }
            // 当前Map在扩容,先协助扩容,在更新值
            else if ((fh = f.hash) == MOVED)
                // 如果bucket已被移动,则帮助进行转移
                tab = helpTransfer(tab, f);
            else {// hash冲突
                // 否则,在已存在的bucket中插入新节点或更新节点值
                V oldVal = null;
                synchronized (f) { // 局部锁,有效减少锁竞争的发生
                    // 验证节点是否仍然存在且未被移动
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            // 链表形式的bucket,进行线程安全的插入或更新操作
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 若节点已经存在,修改该节点的值
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                // 节点不存在,添加到链表末尾
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 如果该节点是 红黑树节点
                        else if (f instanceof TreeBin) {
                            // 红黑树形式的bucket,进行线程安全的插入或更新操作
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 对于链表或树中节点数量达到阈值8的bucket,转换为红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 统计节点个数,检查是否需要resize
        addCount(1L, binCount);
        return null;
    }

2、remove源码

    /**
     * 替换或删除给定键对应的节点值。如果条件匹配(cv非空时),用value替换节点值;
     * 若替换后的值为null,则删除节点。这是四个公开的删除/替换方法的实现基础。
     *
     * @param key 要操作的键
     * @param value 替换节点的新的值,如果为null,则表示删除节点
     * @param cv 条件值,仅当节点当前值与cv匹配时才进行替换
     * @return 返回被替换的旧值,如果没有操作则返回null
     */
    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode()); // 计算键的哈希值
        for (Node<K,V>[] tab = table;;) { // 循环遍历哈希表
            Node<K,V> f; int n, i, fh;
            // 查找指定位置的节点
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            else if ((fh = f.hash) == MOVED) // 处理哈希表正在扩容的情况
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {
                    // 在同步块内进行节点的查找和替换操作
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) { // 链表形式的节点
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) { // 遍历链表
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        // 替换值或删除节点
                                        if (value != null)
                                            e.val = value;
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        else if (f instanceof TreeBin) { // 红黑树形式的节点
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            // 在红黑树中查找节点
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    // 替换值或删除节点
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                // 验证操作是否成功
                if (validated) {
                    if (oldVal != null) {
                        // 更新计数器,当删除节点时
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null; // 没有找到或操作节点时返回null
    }

  • 31
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值