HashMap 源码学习-jdk1.8

1、一些常量的定义

这里针对MIN_TREEIFY_CAPACITY 这个值进行解释一下。

java8里面,HashMap 的数据结构是数组 + (链表或者红黑树),每个数组节点下可能会存在链表和红黑树之间的转换,当同一个索引下面的节点超过8个时,首先会看当前数组长度,如果大于64,则会发生链表向红黑树的 转换,否则不会转换,而是扩容。

    // 默认的初始化长度 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
        
    // 默认的最大容量 2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认的扩容因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 链表转为树的阈值
    static final int TREEIFY_THRESHOLD = 8;

    // 树转为链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;
    
    // map已存节点的数量
    transient int size;

    // 修改次数    
    transient int modCount;

    // 扩容阈值 当size达到这个值的时候,hashmap开始扩容
    int threshold;

    // 加载因子 threshold = 容量 * loadFactor
    final float loadFactor;

2、构造器

HashMap提供了三个构造器。

    // 无参构造器,使用默认加载因子 0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    // 只传入初始化容量,也会使用默认加载因子 0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 同时传入初始化容量和加载因子 (初始化容量要大于0,且不能超过最大容量)
    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;
        // 初始化的容量先赋值给了threshold 暂存。
        this.threshold = tableSizeFor(initialCapacity);
    }

注意看,使用带参构造器 会调用 tableSizeFor(initialCapacity); 这个方法是干嘛的呢?其实就是为了计算初始化容量。HashMap规定,其容量必须是2的N次方

  •  不传初始化容量,就取默认值16
  • 传了初始化容量,则初始化容量设置为大于等于该数值的 一个最小的2的N次方
    • 比如传入了7,不是2的N次方,那么取比他大的最小的2的N次方,就是8
    • 比如传入了8,刚好是2的N次方,那就取8
    • 比如传入了9,不是2的N次方,那么取比他大的最小的2的N次方,就是16
    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;
    }

或等于操作 a |= b ,其实就是 a = a | b。

无符号右移操作:a >>> b 就表示将a向右移动b位,左边空出来的用0补充,右边的被丢弃

那么 n |= n >>> 1 操作得到的结果就是,最高位和次高位的结果为1;--- > n 的前两位为1

n |= n >>> 2 操作之后 --- > n的前四位为1

.... 一通操作之后,得到的值是一个低位全是1的值。然后返回的时候+1,得到的值就是一个比n大的2的N次方。而开头的 int n = cap - 1 是为了解决本身就是2的N次方的场景。

3、插入操作
3.1、插入操作的具体流程
  1. 插入前首先判断数组是否为空,如果为空就进行初始化
  2. 计算key的hash值,然后和数组长度-1 进行 & 运算,获取在数组中的索引位置
    1. 当前位置不存在元素,就直接创建新节点放在当前索引位置
    2. 当前位置元素存在,就走后续的逻辑
  3. 判断当前坐标下头节点的hash值是否和 key的hash相等,如果相等就进行替换(还要判断一个控参 onlyIfAbsent,这个为false的时候才会替换,最常用的put操作这个值就是false )
  4. 如果不相等,判断当前是链表还是红黑树
    1. 如果是链表,遍历链表节点,并统计节点个数:
      1. 如果找到了相同的key,就进行覆盖操作,
      2. 如果没有找到相同key,就将节点添加到链表最后面,并判断是否超过8个节点,如果大于等于8,就要链表转红黑树操作。
    2. 如果是红黑树:找到红黑树根节点,从根节点开始遍历:
      1. 找到相同的key,就进行替换
      2. 找不到相同的key,就放到相应的位置,然后进行红黑树插入平衡调整
  5. 插入完成之后,判断当前节点数目是否超过扩容阈值,如果超过,就进行扩容。
public V put(K key, V value) {
    /**
     * 首先计算出了key的hash 值
     */
    return putVal(hash(key), key, value, false, true);
}

final V putVal ( int hash, K key, V value,boolean onlyIfAbsent, boolean evict){
    HashMap.Node<K, V>[] tab;
    HashMap.Node<K, V> p;
    int n, i;
    /**
     * 判断数组是否为空,为空则进行数组初始化 
     * ---> tab = resize() 然后获取数组的长度
     */
    if ((tab = table) == null || (n = tab.length) == 0) {
        n = (tab = resize()).length;
    }
    /**
     * 计算当前节点要插入数组的索引的位置 ---> (n - 1) & hash
     * 如果索引处不存在节点,就新创建节点放到索引的位置
     */
    if ((p = tab[i = (n - 1) & hash]) == null) {
        tab[i] = newNode(hash, key, value, null);
    }
    /**
     * 如果索引处存在节点,走这个逻辑
     */
    else {
        HashMap.Node<K, V> e;
        K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
            /**
             * 进入这个分支,说明要插入的节点和头节点的key相同
             */
            e = p;
        } else if (p instanceof HashMap.TreeNode) {
            /**
             * 说明头节点是红黑树了,要把这个新节点插入到红黑树中,涉及到新节点的插入,红黑树的平衡调整等
             */
            e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        } else {
            /**
             * 说明头节点是链表节点,遍历链表
             */
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    /**
                     * 遍历到最后了,创建新节点插入到尾端
                     * 还要判断节点是否超过8个,超过了要转化为红黑树
                     */
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    {
                        treeifyBin(tab, hash);
                    }
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    /**
                     * 找到了相同key的value
                     */
                    break;
                }
                p = e;
            }
        }
        /**
         * e不为空,说明有key相同的情况,替换成新的value,然后直接返回旧的节点
         * 因为节点数目不存在变化,因此不需要进行扩容判断
         */
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent的判断
            if (!onlyIfAbsent || oldValue == null) {
                e.value = value;
            }
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    /**
     * 如果当前节点超过了扩容阈值,就进行扩容,然后返回null
     */
    if (++size > threshold) {
        resize();
    }
    afterNodeInsertion(evict);
    return null;
}

3.2、 key的hash值是怎么计算的?为什么要这么计算?
  • 如果key为空,就直接返回0
  • 不为空将 key的hashcode 和 hashcode左移16位进行& 运算
  • ---- 左移16位主要就是为了将hash的高位也参与到hash计算中,减少hash冲突。
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

3.3、resize扩容流程介绍
  1. 首先会对老数组进行一系列的校验,大致分为:
    1. 老数组为空,就设置一下数组长度和扩容阈值,新建数组,然后返回
    2. 老数组不为空,校验老数组长度,如果长度超过上限,扩容阈值修改为int最大值,返回
    3. 否则:容量、扩容阈值变为原来的2倍
  2. 接着开始遍历老数组
    1. 当前坐标下没有节点,就继续遍历
    2. 当前坐标只有一个节点,计算hash值,然后放到新数组对应位置
    3. 当前坐标是链表,走链表逻辑:
      1. 遍历链表节点,计算  e.hash & oldCap ,这个值如果是0,说明扩容后,在新数组的坐标和老数组一样,如果为1 ,说明扩容后在新数组的坐标应该是 老数组坐标 + 扩容长度,因此通过计算这个值,可以将链表节点分为高位节点和低位节点
      2. 定义高位和低位两个链表,不断将链表节点放在这两个新链表尾端
      3. 然后低位链表放在新数组的i 坐标位置,高位链表放在新数组i+oldcap的位置
    4. 当前坐标是红黑树,走红黑树的逻辑
      1. 因为维护红黑树的时候也维护了一个双向链表,因此通过 e.prev e.next就可以遍历整个树 (也就是说遍历链表就等于遍历树)
      2. 同样是将元素分别放在低位链表和高位链表中,并计算每个链表的长度
      3. 低位链表的头节点放在新数组的i坐标位置,然后维护链表的红黑树结构(维护前会判断高位链表是否有值,如果为空,说明树结构没有被破坏而是直接迁移到新数组中了,这个时候就可以不用重新维护树结构了)
      4. 高位链表头节点放在新数组i+oldcap的位置,维护树结构,同3

注意:jdk1.8中,hashmap的扩容,链表节点处理只遍历了一次,而ConcurrentHashMap中遍历了两次。

final HashMap.Node<K, V>[] resize() {
    HashMap.Node<K, V>[] oldTab = table;
    // 临时存储老的数组长度和老的扩容阈值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    // 定义新的数组长度和新的扩容阈值
    int newCap, newThr = 0;
    // oldCap > 0 说明数组已经初始化了
    if (oldCap > 0) {
        // 当前数组长度已经大于等于最大数组长度了,就把扩容阈值设置为int最大值返回,不需要扩容了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 否则,长度变为原来2倍
        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);
    }
    // hashmap 初始化的时候,是将数组初始化长度赋值给了threshold,这里开始才是变成扩容阈值。
    threshold = newThr;
    // 创建新的数组,并将新数组赋值给table
    HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap];
    table = newTab;
    // 老数组不为空,就走扩容逻辑,否则就直接返回新创建的数组了
    if (oldTab != null) {
        // 对老数组开始遍历
        for (int j = 0; j < oldCap; ++j) {
            HashMap.Node<K, V> e;
            // 数组的坐标节点为空说明没数据,直接遍历下个坐标
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 只有个节点,直接取出该节点,计算hash值,放到新数组中
                if (e.next == null) {
                    newTab[e.hash & (newCap - 1)] = e;
                }
                // 当前是红黑树,执行红黑树扩容逻辑
                else if (e instanceof HashMap.TreeNode) {
                    ((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                }
                // 当前是链表,执行链表扩容逻辑
                else { // preserve order
                    // 定义高位链表和低位链表
                    HashMap.Node<K, V> loHead = null, loTail = null;
                    HashMap.Node<K, V> hiHead = null, hiTail = null;
                    HashMap.Node<K, V> next;
                    // 遍历链表
                    do {
                        next = e.next;
                        // e.hash & oldCap 可以计算出当前节点应该放在高位还是低位
                        if ((e.hash & oldCap) == 0) {
                            // 将遍历到的节点放在loTail尾部
                            // loHead指向低位节点的头节点
                            if (loTail == null) {
                                loHead = e;
                            } else {
                                loTail.next = e;
                            }
                            loTail = e;
                        } else {
                            // 将遍历到的节点放在hiTail尾部
                            // hiHead指向高位节点的头节点
                            if (hiTail == null) {
                                hiHead = e;
                            } else {
                                hiTail.next = e;
                            }
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 低位链表的头节点放在 新数组的原index中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 高位链表的头节点放在 新数组的原index + oldCap 中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}


final void split(HashMap<K, V> map, HashMap.Node<K, V>[] tab, int index, int bit) {
    // ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 这个this 就是数组中取出来的第一个元素,也就是树的头节点
    HashMap.TreeNode<K, V> b = this;
    // 设置低位首节点和低位尾节点,高位首节点和高位尾节点
    HashMap.TreeNode<K, V> loHead = null, loTail = null;
    HashMap.TreeNode<K, V> hiHead = null, hiTail = null;

    // 这两个值用于记录低位坐标和高位坐标节点的数目
    int lc = 0, hc = 0;

    // 从根节点开始,对整个树进行遍历,我们介绍了,红黑树其实也维护了双向链表,因此通过 e.prev  e.next就可以遍历整个树
    for (HashMap.TreeNode<K, V> e = b, next; e != null; e = next) {
        next = (HashMap.TreeNode<K, V>) e.next;
        e.next = null;

        // bit传入的就是oldCap,也就是旧数组的长度,通过hash & 运算,就可以判断是放在新数组的低位坐标还是高位坐标
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null) {
                loHead = e;
            } else {
                loTail.next = e;
            }
            loTail = e;
            ++lc;
        } else {
            if ((e.prev = hiTail) == null) {
                hiHead = e;
            } else {
                hiTail.next = e;
            }
            hiTail = e;
            ++hc;
        }
    }

    // 低位坐标处理逻辑
    if (loHead != null) {
        // 低位节点数目小于等于6,就转为链表
        if (lc <= UNTREEIFY_THRESHOLD) {
            tab[index] = loHead.untreeify(map);
        }

        // 否则,还是红黑树结构
        else {
            // 链表头节点赋值给 tab[index]
            tab[index] = loHead;
            if (hiHead != null)
            // 对低位的链表维护红黑树结构
            // 为什么加一个hiHead != null 判断呢?因为如果原来的元素全部都分到了低位节点,那说明树结构没有被破坏,就不需要维护了
            {
                loHead.treeify(tab);
            }
        }
    }

    // 高位和低位的处理一样
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD) {
            tab[index + bit] = hiHead.untreeify(map);
        } else {
            tab[index + bit] = hiHead;
            if (loHead != null) {
                hiHead.treeify(tab);
            }
        }
    }
}

3.4 链表转红黑树

final void treeifyBin(HashMap.Node<K, V>[] tab, int hash) {
    int n, index;
    HashMap.Node<K, V> e;
    // 如果数组为空,或者数组长度小于64,就先尝试扩容,因为链表转树的消耗太大了
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
        resize();
    } 
    // 先拿到当前坐标下的头节点 ,赋值给 e
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 定义头节点和尾节点
        HashMap.TreeNode<K, V> hd = null, tl = null;
        // 遍历链表
        do {
            // 将链表节点转化为红黑树节点
            HashMap.TreeNode<K, V> p = replacementTreeNode(e, null);
            if (tl == null) {
                // 最开始遍历的时候,尾节点肯定为空,就把跟节点指向当前节点
                hd = p;
            } else {
                // 双向链表,将前后节点关联起来
                p.prev = tl;
                tl.next = p;
            }
            // 当前节点设置为尾节点
            tl = p;
        } while ((e = e.next) != null);
        // 截止到目前,把链表中所有的node对象转变为了红黑树节点,单向链表变成了双向链表
        
        // 把转换后的双向链表,替换原来位置上的单向链表
        if ((tab[index] = hd) != null) {
            // 树化操作
            hd.treeify(tab);
        }
    }
}

final void treeify(HashMap.Node<K, V>[] tab) {
    HashMap.TreeNode<K, V> root = null;
    // 因为是调用的hd.treeify(tab),因此,这里的this就是双向链表的头节点,这里先赋值给了临时变量x
    // 开始循环这个双向链表了,x就是循环的元素,next就是下一个节点元素
    for (HashMap.TreeNode<K, V> x = this, next; x != null; x = next) {
        next = (HashMap.TreeNode<K, V>) x.next;
        // 当前节点左右孩子都设置为空
        x.left = x.right = null;
        if (root == null) {
            // 第一次进来,根节点肯定是空,将头节点设置为根节点,染色黑
            x.parent = null;
            x.red = false;
            root = x;
        } 
        // 第一次以后的循环都走下面的分支了
        else {
            // 定义当前节点的key 和 hash
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            // 开始遍历树结构了
            for (HashMap.TreeNode<K, V> p = root; ; ) {
                // ph 和 pk 定义当前树节点的 hash 和 key ,通过hash判断当前节点要放在树的左边还是右边
                // dir代表 往树左边放还是右边放
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h) {
                    dir = -1;
                } else if (ph < h) {
                    dir = 1;
                } 
                // hash相等的时候,继续一系列的判断,最终得到dir
                else if ((kc == null && (kc = comparableClassFor(k)) == null)
                    || (dir = compareComparables(kc, k, pk)) == 0) {
                    dir = tieBreakOrder(k, pk);
                }

                HashMap.TreeNode<K, V> xp = p;
                // dir <= 0 说明是在左侧,否则是在右侧
                // 只有保证当前树节点没有对应的左孩子或者右孩子的时候,才会将当前节点挂上去,否则继续循环遍历树结构
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0) {
                        xp.left = x;
                    } else {
                        xp.right = x;
                    }
                    // 红黑树平衡操作
                    root = balanceInsertion(root, x);
                    // 当前节点已经插入红黑树中了,可以跳出当前循环,遍历链表的下一个节点
                    break;
                }
            }
        }
    }
    // 把root节点放在当前坐标位置
    moveRootToFront(tab, root);
}

/**
 * 我们要明确,红黑树节点不但维护了树结构,还维护了双向链表的结构
 * 这个方法的作用就是:
 * 1、将树的根节点,赋值给tab[i]
 * 2、将这个节点,变成双向链表的头节点
 */
static <K, V> void moveRootToFront(HashMap.Node<K, V>[] tab, HashMap.TreeNode<K, V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        // 通过根节点 hash计算在数组中的索引位置
        int index = (n - 1) & root.hash;
        // 取到当前索引的第一个节点
        HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index];
        
        // 如果root节点和 当前索引位置第一个节点不一样,就把root节点放在当前坐标位置
        // 同时要维护双向链表,将root节点变成双向链表的第一个节点。
        if (root != first) {
            HashMap.Node<K, V> rn;
            tab[index] = root;
            // 将root节点变成双向链表的第一个节点。
            HashMap.TreeNode<K, V> rp = root.prev;
            if ((rn = root.next) != null) {
                ((HashMap.TreeNode<K, V>) rn).prev = rp;
            }
            if (rp != null) {
                rp.next = rn;
            }
            if (first != null) {
                first.prev = root;
            }
            root.next = first;
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}
4、删除操作
  1. 数组没有初始化,或者对应下标节点为空,说明没有该元素,直接返回null
  2. 查找node,(红黑树或者链表结构)
  3. 删除node,(红黑树或者链表结构) --- 删除节点的时候即便树的元素小于等于6也不会转为链表,在代码里面没看到,只在扩容的时候会有转换操作。
/**
 * 删除方法,主要介绍以下两个参数
 * @param matchValue true:只有值也相同的时候才删除
 * @param movable true:删除后移动节点,树结构的时候会用到,一般为true
 */
final HashMap.Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    HashMap.Node<K, V>[] tab;
    HashMap.Node<K, V> p;
    int n, index;
    // 组数没有初始化,或者对应坐标下面没有元素,直接返回null了
    if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
        // node记录要删除的元素
        HashMap.Node<K, V> node = null, e;
        K k;
        V v;
        // 找要删除的元素,赋值给node
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
            node = p;
        } else if ((e = p.next) != null) {
            if (p instanceof HashMap.TreeNode) {
                // 从树中查找节点
                node = ((HashMap.TreeNode<K, V>) p).getTreeNode(hash, key);
            } else {
                // 从链表中查找节点 ,链表结构时,p是node的前置节点
                do {
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            // node不为空的时候,删除节点
            if (node instanceof HashMap.TreeNode) {
                ((HashMap.TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
            } else if (node == p) {
                tab[index] = node.next;
            } else {
                p.next = node.next;
            }
            // 修改次数加1,size减一,返回删除的node
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/402762.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Elastic Stack--01--简介、安装

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1. Elastic Stack 简介为什么要学习ESDB-Engines搜索引擎类数据库排名常年霸榜![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/051342a83f574c8c910cda…

【软件架构】02-复杂度来源

1、性能 1&#xff09;单机 受限于主机的CPU、网络、磁盘读写速度等影响 在多线程的互斥性、并发中的同步数据状态等&#xff1b; 扩展&#xff1a;硬件资源、增大线程池 2&#xff09;集群 微服务化拆分&#xff0c;导致调用链过长&#xff0c;网络传输的消耗过多。 集…

【Web前端笔记10】CSS3新特性

10 CSS3新特性 &#xff11;、圆角 &#xff12;、阴影 &#xff08;&#xff11;&#xff09;盒阴影 &#xff13;、背景渐变 &#xff08;&#xff11;&#xff09;线性渐变&#xff08;主要掌握这种就可&#xff09; &#xff08;&#xff12;&#xff09;径向渐变 &…

滚雪球学Java(67):深入理解 TreeMap:Java 中的有序键值映射表

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

C++学习之list容器

C++ list基本概念 在C++中,std::list是一个双向链表(doubly linked list)容器,它包含在 <list> 头文件中。下面是一些关于C++ std::list的基本概念: 双向链表结构:std::list是由多个节点组成的双向链表结构,每个节点包含数据元素和指向前一个节点和后一个节点的指…

ABCDE联合创始人BMAN确认出席Hack .Summit() 2024香港Web3盛会

ABCDE联合创始人和普通合伙人BMAN确认出席Hack .Summit() 2024&#xff01; ABCDE联合创始人和普通合伙人BMAN确认出席由 Hack VC 主办&#xff0c;并由 AltLayer 和 Berachain 联合主办&#xff0c;与 SNZ 和数码港合作&#xff0c;由 Techub News 承办的Hack.Summit() 2024区…

穿越Redis单线程迷雾:从面试场景到技术内核的解读

目录 ​编辑 前言 Redis中的多线程 I/O多线程 Redis中的多进程 结论 延伸阅读 前言 很多人都遇到过这么一道面试题&#xff1a;Redis是单线程还是多线程&#xff1f;这个问题既简单又复杂。说他简单是因为大多数人都知道Redis是单线程&#xff0c;说复杂是因为这个答案…

Kotlin学习 6

1.接口 interface Movable {var maxSpeed: Intvar wheels: Intfun move(movable: Movable): String}class Car(var name: String, override var wheels: Int 4, _maxSpeed: Int) : Movable {override var maxSpeed: Int _maxSpeedget() fieldset(value) {field value}overr…

C++ Primer 笔记(总结,摘要,概括)——第4章 表达式

目录 4.1 基础 4.1.1 基本概念 4.1.2 优先级与结合律 4.1.3 求值顺序 4.2 算术运算符 4.3 逻辑和关系运算符 4.4 赋值运算符 4.5 递增和递减运算符 4.6 成员访问运算符 4.7 条件运算符 4.8 位运算符 4.9 sizeof运算符 4.10 逗号运算符 4.11 类型转换 4.11.1 算数转换…

Java的编程之旅19——使用idea对面相对象编程项目的创建

在介绍面向对象编程之前先说一下我们在idea中如何创建项目文件 使用快捷键CtrlshiftaltS新建一个模块&#xff0c;点击“”&#xff0c;再点New Module 点击Next 我这里给Module起名叫OOP,就是面向对象编程的英文缩写&#xff0c;再点击下面的Finish 点Apply或OK均可 右键src…

day3:界面跳转,qss与对话框

思维导图 完善对话框&#xff0c;点击登录对话框&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示”登录成功“&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳转到其他界面 如果账号和密码不匹配…

成像光谱遥感技术中的AI革命:ChatGPT应用指南

遥感技术主要通过卫星和飞机从远处观察和测量我们的环境&#xff0c;是理解和监测地球物理、化学和生物系统的基石。ChatGPT是由OpenAI开发的最先进的语言模型&#xff0c;在理解和生成人类语言方面表现出了非凡的能力。重点介绍ChatGPT在遥感中的应用&#xff0c;人工智能在解…

【Effective Objective - C】—— 系统框架

【Effective Objective - C】—— 系统框架 47.熟悉系统框架CoreFoundation框架其他框架要点 48. 多用块枚举&#xff0c;少用for循环for循环使用Objective-C 1.0的NSEnumerator遍历快速遍历基于块的遍历方式要点 49.对自定义其内存管理语义的collection使用无缝桥接要点 50.构…

虚拟机器centos7无法识别yum 命令异常处理笔记

问题现象 启动虚拟机后执行ipconfig 提示未找到该命令,然后执行yum install -y net-tools提示 curl#6 - "Could not resolve host: mirrorlist.centos.org; 未知的错误"的错误 [roothaqdoop~]# ifconfig -bash: ifconfig: 未找到命令 [roothadoop~]# yum install …

【QT 5 +Linux下软件桌面快捷方式+qt生成软件创建桌面图标+学习他人文章+第二篇:编写桌面文件.desktop】

【QT 5 Linux下软件桌面快捷方式qt生成软件创建桌面图标学习他人文章第二篇&#xff1a;编写桌面文件.desktop】 1、前言2、实验环境3、自我学习总结-本篇总结1、新手的疑问&#xff0c;做这件事目的2、了解.desktop3、三个关键目录以及文件编写1、目录&#xff1a;/opt/2、目录…

ChatGPT回答模式

你发现了吗&#xff0c;ChatGPT的回答总是遵循这些类型方式。 目录 1.解释模式 2.类比模式 3.列举模式 4.限制模式 5.转换模式 6.增改模式 7.对比模式 8.翻译模式 9.模拟模式 10.推理模式 1.解释模式 ChatGPT 在回答问题或提供信息时&#xff0c;不仅仅给出…

智能科技助力服装业:商品计划管理系统的革命性变革

随着智能科技的飞速发展&#xff0c;服装行业正在经历前所未有的变革。在这股浪潮中&#xff0c;商品计划管理系统的智能化转型成为了行业的核心驱动力。这种变革不仅极大地提高了服装企业的运营效率和市场竞争力&#xff0c;更为整个行业的可持续发展注入了新的活力。 智能商…

相机图像质量研究(35)常见问题总结:图像处理对成像的影响--运动噪声

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

T-Dongle-S3开发笔记——移植LVGL

添加lvgl组件 idf.py add-dependency lvgl/lvgl>8.* 新建终端执行命令后出现了新的文件&#xff1a; 清除再编译后才会出现lvgl库 优化为本地组件 以上方式修改了组件文件内容重新编译后文件又会变回去。 所以我们要把lvgl变成本地组件 1、要把 idf_component.yml 文…

【Python】遇到的一些小问题及解决办法汇总

【工具】&#xff1a;pycharm 【环境】&#xff1a;Windows 一、数据集路径导入报错 【错误提示】&#xff1a; SyntaxError: (unicode error) ‘unicodeescape’ codec can’t decode bytes in position 2-3: truncated \UXXXXXXXX escape 如图&#xff1a; 【原因分析】&a…