万字源码解析!彻底搞懂 HashMap【二】:putVal 方法和 resize 方法(重点)

HashMap 的底层原理和扩容机制一直都是面试的时候经常被问到的问题,同时也是集合源码中最难阅读的一部分😢,之前更新的 ArrayList 源码阅读收获了很多朋友的喜欢,也给了我很多自信;本次我准备完成一个关于 HashMap 源码阅读和解析的专栏,共分为四部分内容:

  1. 概念辨析、属性和构造方法的源码阅读
  2. putVal() 和 resize() 方法的源码分析
  3. 详细讲解红黑树
  4. HashMap 中与树有关的方法

通过阅读这些内容相信大家一定可以彻底的搞懂 HashMap,如果喜欢文章的话不要忘记订阅专栏和关注。😊

前言:
本篇是专栏的第二部分:putVal 方法和 resize 方法,这是 HashMap 源码中最重要的两个方法,涉及到初始化和扩容机制等重点的知识,大家在阅读之前需要对 Java 中的位运算有一定的了解,同时需要首先阅读上一篇博客,对基本的常量和构造方法有基本的理解和把握。
上篇博客导航:
万字源码解析!彻底搞懂 HashMap【一】:概念辨析与构造方法源码解析

文章目录

    • 3.源码阅读第二部分- putVal()与 resize() 方法
      • 3.1 put() 方法
      • 3.2 resize() 方法

3.源码阅读第二部分- putVal()与 resize() 方法

3.1 put() 方法

     /**
      * Associates the specified value with the specified key in this map.
      * If the map previously contained a mapping for the key, the old
      * value is replaced.
      */
     public V put(K key, V value) {
         return putVal(hash(key), key, value, false, true);

put() 方法就是向 HashMap 中插入数据的方法 put() 方法中实际调用的是 putVal() 方法,在参数中还调用了 hash(key) 方法,先来阅读一下这个方法:

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

将语句拆分一下可以得到如下的代码

 int h;
 if (key == null) {
     return 0;
 } else {
     h = key.hashCode();
     return h ^ (h >>> 16);
 }

如果 key 是 null,那就直接映射为 0,否则就执行 hashCode() 方法获得哈希值(有可能是 native 也有可能是重写的方法),存入 h 中。

然后将 h 和 h 右移 16 位获得的数做 按位异或 操作,这一步的作用在哪里呢?

首先来回顾一下上面提到果的路由算法

 (table.length - 1) & node.hash

当数组长度很小的时候,使用到的 h 的位数就越少,此时就会导致重复的概率大大增大;此时将 h 与 h >>> 16 进行按位异或操作,可以将 高位的数字参与到运算当中,来降低哈希冲突的概率,这其实就是扰动函数,然后将这个值返回作为实际被使用的哈希值。

接下来让我们进入 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;
         if ((tab = table) == null || (n = tab.length) == 0) // 说明此时还未进行过初始化
             n = (tab = resize()).length;
         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)))) // 本节点的 key 和本次插入的 key 相同(替换操作)
                 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) { // 遍历过程中没有找到与插入节点 key 相同的情况
                         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 相同的节点
                         break;
                     p = e;
                 }
             }
             if (e != null) { // 针对与 替换 操作的逻辑
                 V oldValue = e.value;
                 if (!onlyIfAbsent || oldValue == null)
                     e.value = value;
                 afterNodeAccess(e);
                 return oldValue;
             }
         }
					// 针对非替换操作的逻辑
         ++modCount;
         if (++size > threshold)
             resize();
         afterNodeInsertion(evict);
         return null;
     }

对于这种长方法,首先要理清楚整个方法的脉络,这里给出一个大致的流程图,通过流程图和上面的注释大致熟悉一下每一块代码的作用,下面正式进入源码的解析。
在这里插入图片描述

 Node<K,V>[] tab; // 指向当前 HashMap 中的 table 的指针
 Node<K,V> p;  // 当前散列表中的元素
 int n, i; // n 表示散列表数组的长度,i 表示路由寻址的结果

参考上面给出的注释,首先先来了解一下方法中使用到了哪些变量

 if ((tab = table) == null || (n = tab.length) == 0)
             n = (tab = resize()).length;

然后来看第一个 if 语句,首先将 tab 指向了本 HashMap 的 table,然后判断它是否为 null,同时将 table 的长度赋值给 n;if 中判断的是 table 是否被初始化,如果没有被初始化就执行 if 代码块中的部分,进行table的初始化,其中的 resize() 也是很重要的方法,这个放到后面详细讲解。


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

然后继续看下一个 if 语句,i = (n - 1) & hash 这个式子大家一定不陌生了,这就是上面提到的路由寻址,找到对应的地址后将该索引位置的元素赋值给 p。

此时 p 有两种可能:null 和 非 null,如果是 null 说明这个索引是第一次使用,执行代码快中的逻辑,直接给tab[i] 赋值即可,这是最简单的情况;如果不是 null 则说明该位置上有数据,有数据同样也有两种可能

  • 出现了哈希冲突,需要往后延申链表
  • 就是完全相同的 key,执行的是替换的操作

下面的代码就是针对这些情况进行的处理。


 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) // -1 for 1st
                             treeifyBin(tab, hash);
                         break;
                     }
                     if (e.hash == hash &&
                         ((k = e.key) == key || (key != null && key.equals(k))))
                         break;
                     p = e;
                 }
             }
             if (e != null) { // 针对与 替换 操作的逻辑
                 V oldValue = e.value;
                 if (!onlyIfAbsent || oldValue == null)
                     e.value = value;
                 afterNodeAccess(e);
                 return oldValue;
             }
         }

仍然是一个很长的部分,让我们来逐步拆解一下:

 Node<K,V> e; K k; // e:如果执行的是替换的操作,e 中存储的就是要替换的节点的地址,k:临时变量

先参考上面的注释了解一下使用到的变量,然后正式进入这一部分的阅读:

     if (p.hash == hash &&
         ((k = p.key) == key || (key != null && key.equals(k))))
         e = p;

回顾一下此时的境况,我们要插入节点的索引上有内容,p 为当前索引上的节点,首先来判断它是否和传入的 key 是相同的,如果是相同的,那进行的其实就是替换的操作。

e 就是来存储要替换的节点的索引的,如果它不为空就说明要执行替换的操作。
注意来看一下 HashMap 中判断两个元素是否相同的逻辑:

  1. 判断两个元素的哈希值是否相同
  2. 调用 equals 方法来判断两个元素是否相同

这其实就牵扯出来一个非常经典的面试题,为什么重写 equals 方法的同时也要重写 hashCode 方法:

在 Object 类中提供的默认的 equals 方法是比较的内存,而 hashCode 方法通过哈希函数映射内存来得到哈希值的,所以如果使用默认的方法,那比较的就都是内存,判断两个元素是否相同的依据就是它们处于同一块内存之中

但是这样的判断方法太过于绝对了,比如我有一个 Staff 类,有两个属性,IdentityCard(id号)、name(姓名),我希望 id 号 和 姓名均相等的两个员工被视作同一名员工,此时通过内存的比较肯定就无法实现了,此时就需要重写 equals 方法,但是在 HashMap 中,首先去比较的还是哈希值,所以我们同时要保证拥有相同 id 号和相同姓名的两个实例要被映射为同一个哈希值,此时就需要去重写 hashCode 方法,将这两个方法都按照我们的逻辑进行重写就能保证它们会被识别为相同的对象。


 else if (p instanceof TreeNode)
                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

这里是为了判断 p 是不是树节点(红黑树),关于树的方法都放在红黑树讲解之后在进行解析。


 else {
     for (int binCount = 0; ; ++binCount) {
         if ((e = p.next) == null) {
             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))))
             break;
         p = e;
     }
 }

如果前两个条件都不符合的话,就尝试去遍历这个节点上的链表:

如果 p.next() 为 null 的时候,就说明已经遍历到了链表的最后一个节点,此时还没有找到,说明这是一个新的需要插入的节点,就将其插入到 p.next() 的位置。

但是如果发现 e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))) 就说明在链表中查找到了相同的 key,此时执行的是 替换操作,此时就用 e 记录下来需要进行替换的节点。


     if (e != null) { // existing mapping for key
         V oldValue = e.value;
         if (!onlyIfAbsent || oldValue == null)
             e.value = value;
         afterNodeAccess(e);
         return oldValue;
     }

执行完上面的逻辑之后,去判断 e 是否为空,如果不为空,就说明需要执行替换的操作,直接执行 if 代码块中的方法来进行替换,替换完成后是直接 return 将方法结束,不会执行后面的流程,返回的是节点原本的 value。

但如果执行的不是替换操作的话,就执行如下的逻辑:

         ++modCount;
         if (++size > threshold)
             resize();
         afterNodeInsertion(evict);
         return null;

首先对 modCount 进行自增(替换操作就不需要自增,因为不改变结构);然后判断当前长度是否需要进行扩容,如果需要就执行扩容的操作。

3.2 resize() 方法

final Node<K,V>[] resize() {
// 第一部分:确定 newCap 和 newThr  的值
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            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;
        
// 第二部分:将原数组中内容复制到拓展后的新数组中
        @SuppressWarnings({"rawtypes","unchecked"})
        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) { // 如果不为 null 就需要进行迁移的操作
                    oldTab[j] = null;
                    if (e.next == null) // 仅有一个节点的情况
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) // 是树节点的情况
                        ((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;
    }

resize() 代码看起来很长,但是其执行的流程相比于 putVal() 方法要简单很多,代码总体分为两个部分:

  1. 确定新的容量(newCap)和新的扩展阈值(newThr)
  2. 如果需要的话,将旧数组中的内容转移到拓展后的数组中

首先还是来分析一下方法中用到的变量:

         Node<K,V>[] oldTab = table; // 指向当前数组的指针
         int oldCap = (oldTab == null) ? 0 : oldTab.length; // resize 之前 table 的长度
         int oldThr = threshold; // resize 之前扩容阈值的大小
         int newCap, newThr = 0; // 新的 table 长度和新的阈值大小

可以看出上面的 newCap 和 newThr 都是没有赋值的,紧接着后面一段代码就是给这两个值赋值:

     if (oldCap > 0) {
         // 普通的扩容机制
         if (oldCap >= MAXIMUM_CAPACITY) {
             // 如果旧的容量已经大于等于最大容量
             threshold = Integer.MAX_VALUE; // 将扩容阈值设定为 Integer.MAX_VALUE
             return oldTab; // 结束扩容
         }
         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
             // 二倍扩容
             newThr = oldThr << 1; // double threshold
     }

首先去判断原本的数组中是否有内容,如果没有就不需要执行转移的操作

然后判断旧的容量是否已达到最大容量 MAXIMUM_CAPACITY,如果已经达到这个容量 2 的 31 次方 - 1 就将下一次的扩容阈值设定为 Integer.MAX_VALUE,此时不会去拓展原数组,直接结束本次的扩容操作。


     else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
              oldCap >= DEFAULT_INITIAL_CAPACITY)
         // 二倍扩容
         newThr = oldThr << 1; // double threshold

这里的 else-if 语句是为了排除 容量极大容量极小 的情况,其中的语句是针对容量适中的情况做处理:

  • 如果 (newCap = oldCap << 1) >= MAXIMUM_CAPACITY 就说明扩容之后容量大于最大容量限制 MAXIMUM_CAPACITY
  • 如果 oldCap < DEFAULT_INITIAL_CAPACITY 就说明容量没有达到默认的初始容量(16)说明此时 oldThr 还是 0

对于如上的两种情况,要在后面进行特殊的处理;如果不在这两种情况之中就执行代码块中的语句,直接扩容二倍。


         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);
         }

此时 oldCap > 0 的情况已经处理完成了,现在要处理的是 oldCap == 0 的情况,也就是第一次扩容的情况

首先处理的是 oldThr > 0 的情况,在未初始化的情况下,通过上面阅读过的源码可以知道,如果此时 oldThr 有值,那这个值一定是在前面的构造方法中 指定的容量,这个容量被暂存在 oldThr ****中,在第一次扩容的时候发挥作用

如果 oldThr 没有被赋值,就使用默认的容量(DEFAULT_INITIAL_CAPACITY)和默认的扩容阈值(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)。


此时就已经给 newCap 和 newThr 赋值了,但是有一种情况是未被对 newThr 赋值的,就是容量过大或者oldThr为0的情况 (newCap = oldCap << 1) >= MAXIMUM_CAPACITY || oldCap < 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;

首先将 newCap * loadFactor 容量乘上负载因子

  • 如果oldThr为0的时候(newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY)就将 ft 赋值给 newThr
  • 否则,就是容量过高的情况,newThr 就设定为 Integer.MAX_VALUE

此时 newCap 和 newThr 就已经被指定了,余下的代码大家猜测一下也能知道是做什么的,就是将旧的散列表中的数据复制到新的散列表中,了解这个目的再去阅读就会简单很多。

// 第二部分:将原数组中内容复制到拓展后的新数组中
        @SuppressWarnings({"rawtypes","unchecked"})
        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) { // 如果不为 null 就需要进行迁移的操作
                    oldTab[j] = null;
                    if (e.next == null) // 仅有一个节点的情况
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) // 是树节点的情况
                        ((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;
                        }
                    }
                }
            }
        }

下面开始对这一大段代码进行详细的讲解:

         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
         table = newTab;

首先根据上面计算出来的 newCap 构造了一个新的更大的数组;然后去判断是否需要进行数据的转移,如果 oldTable 不为空的话就需要遍历数组来进行转移,如果遍历到非空的节点就进行转移的逻辑

对于一个节点有三种情况,分别对应着代码中的 if、if-else 和 else 语句段:

     if (e.next == null)
         // 那在更高位的路由中也不会出现哈希冲突,计算路由直接插入
         newTab[e.hash & (newCap - 1)] = e;

如果 e.next == null 的话就说明此时这个索引下面仅有一个元素,此时就计算它在新数组中的索引然后直接插入即可;这是第一种情况。


     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 {
         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;
     }
 }

然后就是最后一种情况,处理链表,这一块理解起来可能有些困难,我们先来了解一下其中用到的临时节点:

     Node<K,V> loHead = null, loTail = null;
     Node<K,V> hiHead = null, hiTail = null;

其中的 lo 是 low 的简写,hi 是 high 的简写,分别代表着高位和低位,比如说原本数组的长度为 16,此时拓展就会将长度拓展为 32,那原本的那一段长度 1 到 16 就是低位,17 到 32 这部分就是高位;上面 loHead 就是处于低位节点的头部,loTail 就是处于低位的节点的尾部,hiHead 和 hiTail 就是高位的头部和尾部。

那为什么要有高位和低位的区分呢?注意我们此时遍历的是一个链表,它在数组的相同位置上,也就说明 它们的哈希值与 (数组长度 - 1) 异或 之后得到的值是 相同 的。

在这里插入图片描述

比如说还是 16 拓展到 32 的情况,一个节点在新的数组中位与高位还是低位取决于上图中这个新的被使用到的部分是 0 还是 1,如果是 0 的话,与新的全 1 执行按位与的操作,得到的结果与其在原数组时的下标相同,反之则会处于一个新的下标(原下标 + 16)。

源码中通过这个方式判断新的一位是 1 还是 0:(e.hash & oldCap) == 0 它将 oldCap(一定是 2 的幂次 0B100000…)和哈希做了一个按位与的操作,如果这个新的部分是 0 的话,经过与的操作得到的结果就是 0,此时其位于低位,反之则位于高位,当 while 循环结束后会得到两个链表,然后将它们插入新的数组中:

     if (loTail != null) {
         loTail.next = null;
         newTab[j] = loHead;
     }
     if (hiTail != null) {
         hiTail.next = null;
         newTab[j + oldCap] = hiHead;
     }

下标也与上面推导的结果相同,最终返回生成的 table。

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

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

相关文章

CST操作教程|精简仿真结果容量和隐藏结构的加密保护功能

使用Archive As精简仿真结果容量 结果保持不变&#xff0c;缩小仿真结果容量的方法。 File > Project > Archive As simulation后保存数据时仿真文件容量太大很是让人头大。为什么文件容量变这么大呢?通常不是因为CST图标形状的.cst文件造成的&#xff0c;而是因为生…

C++set和map详细介绍

文章目录 前言一、关联式容器和序列式容器二、set1.set文档介绍2.set成员函数1.构造函数2.迭代器3.容量4.修改5.其他 三.multiset四.map1.map文档介绍2.map成员函数1.构造2.insert插入3.count4.迭代器5.【】和at 五.multimap总结 前言 在本篇文章中&#xff0c;我们将会学到关…

DC-DC芯片D1509适用于工控主板、TV板卡、安卓主板、车载功放电源等产品方案应用。

一、应用领域 适用于工控主板、TV板卡、安卓主板、车载功放电源等产品方案应用。 二、功能介绍 D1509是芯谷科技推出的一款输入耐压40V、输出电压1.23-37V可调、输出电流最大2.0A的高效率、高精度DC-DC芯片&#xff0c;其输出电压有固定3.3V、5.0V和12.0V的版本&#xff…

BM96 主持人调度(二)(贪心算法)

一开始写的时候忘了给start、end数组赋值了 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可** 计算成功举办活动需要多少名主持人* param n int整型 有n个活动* param start…

C#:用定时器监控定时器,实现中止定时器正在执行的任务,并重启

Windows服务中使用的比较多的是定时器&#xff0c;但这种定时任务有个比较大的毛病&#xff1a;有时会莫名其妙地停止执行&#xff08;长时间执行不完&#xff0c;假死&#xff09;&#xff0c;必须得手工重启Windows服务才能恢复正常。这个就太麻烦了。 有没有办法来实现定时…

Vue3快速上手 详细内容

Vue3快速上手 1.Vue3简介 2020年9月18日&#xff0c;Vue.js发布3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;海贼王&#xff09;耗时2年多、2600次提交、30个RFC、600次PR、99位贡献者github上的tags地址&#xff1a;Release v3.0.0 One Piece vuejs/core Git…

什么是国密SSL证书,和其他SSL证书的区别?

我们要了解什么是SSL证书。SSL&#xff08;Secure Sockets Layer&#xff0c;安全套接层&#xff09;是一种安全协议&#xff0c;主要用于在互联网上对通信双方进行身份验证以及保障数据的安全传输。而SSL证书则是由权威的数字证书认证机构签发的&#xff0c;用于证明网站身份的…

【Linux】磁盘与文件系统管理

目录 一、 磁盘结构 1. 数据结构 2. 物理结构 3. 硬盘的接口类型 二、 如何使用Linux中的磁盘 三、 文件系统 四、 磁盘分区 1. MBR分区 2. 分区的优缺点 3. 磁盘及分区的管理工具 五、格式化与挂载 1. 格式化 2. 挂载 六、实例演示 1. 演示分区格式化挂载 2. …

C语言写流星雨代码

目录 需要包含的头文件 图片素材的路径 初始化背景图片 报错怎么解决&#xff1f; 初始化流星雨 放置流星雨图片 让流星雨动起来 总不能让流星砸到地面吧 是不是应该再来一点背景音乐&#xff1f; 所有代码 需要包含的头文件 IMAGE img;//创建流星雨的图片变量void…

HTML - 请你说一下如何阻止a标签跳转

难度级别:初级及以上 提问概率:55% a标签的默认语义化功能就是超链接,HTML给它的定位就是与外部页面进行交流,不过也可以通过锚点功能,定位到本页面的固定id区域去。但在开发场景中,又避免不了禁用a标签的需求,那么都有哪些方式可以禁用…

Jmeter针对多种响应断言的判断

有时候response返回的结果并非一种&#xff0c;有多种&#xff0c;需要对这几种进行判断的时候需要使用Bean Shell。 &#xff08;1&#xff09;首先获取响应数据 String response prev.getResponseDataAsString(); ResponseCode 响应状态码 responseHeaders 响应头信息 res…

DFS:深搜+回溯+剪枝解决排列、子集问题

创作不易&#xff0c;感谢三连支持&#xff01;&#xff01; 一、全排列I . - 力扣&#xff08;LeetCode&#xff09; class Solution { public://全局变量vector<vector<int>> ret;vector<int> path;bool check[6];vector<vector<int>> perm…

虚拟网络设备性能优化

在现代网络架构中&#xff0c;虚拟网络设备扮演着越来越重要的角色&#x1f310;&#xff0c;特别是在云计算☁️和容器化技术&#x1f4e6;广泛应用的背景下。虚拟网络设备如虚拟以太网设备&#xff08;veth&#xff09;、虚拟交换机&#xff08;vSwitch&#xff09;、和虚拟路…

YOLOv9综合指南

YOLOv9是YOLO系列中用于实时目标检测的最新进展&#xff0c;引入了可编程梯度信息&#xff08;PGI&#xff09;和通用高效层聚合网络&#xff08;GELAN&#xff09;等新技术来解决信息瓶颈并提高检测精度和效率。 在这篇文章中&#xff0c;我们研究了 YOLOv9 的一些关键优势。 …

Java并发编程: Java线程组(ThreadGroup)

文章目录 一、介绍二、线程组特性1、关联性&#xff08;1&#xff09;一级关联性&#xff08;2&#xff09;多级关联性 2、自动归属属性3、根线程组 三、线程组作用1、统一异常处理机制 一、介绍 Java线程组&#xff08;ThreadGroup&#xff09;是一种用于组织和管理线程的机制…

【计算机毕业设计】在线商品管理系统的设计与实现——后附源码

&#x1f389;**欢迎来到琛哥的技术世界&#xff01;**&#x1f389; &#x1f4d8; 博主小档案&#xff1a; 琛哥&#xff0c;一名来自世界500强的资深程序猿&#xff0c;毕业于国内知名985高校。 &#x1f527; 技术专长&#xff1a; 琛哥在深度学习任务中展现出卓越的能力&a…

代码随想录算法训练营第三十四天| LeetCode 1005.K次取反后最大化的数组和、134. 加油站、135. 分发糖果

一、LeetCode 1005.K次取反后最大化的数组和 题目链接/文章讲解/视频讲解&#xff1a;https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%90%8E%E6%9C%80%E5%A4%A7%E5%8C%96%E7%9A%84%E6%95%B0%E7%BB%84%E5%92%8C.html 状态&#xff1a;已解决 1.思路 还是那个…

基于SpringBoot+vue的在线商城系统+论文+免费远程调试

基于SpringBootvue的在线商城系统034(含源码 数据库文档免费送&#xff09; 开发系统:Windows10 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springb…

【SCI绘图】【热力图系列1 R】多特征相关性分析热力图R语言实现

SCI&#xff0c;CCF&#xff0c;EI及核心期刊绘图宝典&#xff0c;爆款持续更新&#xff0c;助力科研&#xff01; 本期分享&#xff1a; 【SCI绘图】【热力图系列1 R】多特征相关性分析热力图R语言实现 1.环境准备 library(gplots) library(RColorBrewer) 2.数据示例 ###…

MySQL典型示例

目录 1.使用环境 2.设计表 3.创建表 4.准备数据 5.查询 1.使用环境 数据库&#xff1a;MySQL 8.0.30 客户端&#xff1a;Navicat 15.0.12 2.设计表 假设我们已经建好了一个名为test的数据库。我们添加如下几个表&#xff1a;教师、课程、学生、班级、成绩。实体联系图设…