【从零开始学习JAVA集合 | 第一篇】深入解读HashMap源码(含面试题)

目录

目录

前言:

HashMap简介:

HashMap的常用常量和变量: 

HashMap的重要考点: 

HashMap的存储过程:

HashMap的扩容过程:

HashMap的初始化:

常见面试题:

总结:


前言:

HashMap 是 Java 中最常用的数据结构之一,它提供了快速的查找、插入和删除操作,可以满足大多数情况下的需求。然而,要深入理解 HashMap 的内部实现和工作原理并不是一件容易的事情。本篇文章旨在深入解读 HashMap,包括其内部结构、工作原理、常见的使用场景以及性能优化等方面。

通过本文的阅读,读者将能够更好地掌握 HashMap 的工作原理,了解其在实际项目中的应用,并能够在需要时根据特定需求进行合理的选择和优化。希望本文能够成为读者学习和掌握 HashMap 的良好起点,为大家在日常编程中应用 HashMap 提供指导和帮助。

HashMap简介:

HashMap 是 Java 中的一个常用数据结构,用于存储键值对(key-value pairs)。它基于哈希表(hash table)实现,允许 null 键和 null 值,并且具有快速的查找、插入和删除操作。HashMap 继承自抽象类 AbstractMap,并且实现了 Map 接口。

这里有一个有趣的知识点:HashMap既继承了,又实现了Map接口。但是明明AbstractMap就已经实现了Map接口,为什么HashMap不直接使用父类AbstractMap的Map接口呢?

据Java集合框架创始人所说:自己在写这段逻辑代码的时候,错误的使用了这种方式,但当时以为  不让HashMap直接继承父类的接口可能在以后会有用,而直到现在这种方式也没有表现出什么作用,而目前的JDK维护者认为其是一个无关紧要的问题,因此也就没有对这里进行修改。

HashMap 的内部结构主要由数组和链表。它通过计算键的哈希值来决定存储位置,使用链表法来解决哈希冲突。当链表的长度大于等于8并且数组的长度大于64的时候,链表就会转为红黑树

当链表的长度等于大于8,但是数组的长度小于64的时候,此时会选择对数组进行扩容而不是将链表转化为红黑树,这是因为在数据量比较小的情况下,使用红黑树反而会降低查询效率。

我们来总结一下HashMap的特点:

  • 存取无序
  • 键和值的位置都可以是null,但是键的位置只能是一个null
  • 键的位置是唯一的,底层的数据结构控制键
  • JDK1.8之前的数据结构是:链表+数组,JDK 1.8后是链表+数组+红黑树
  • 链表长度大于8并且数组长度大于64,才会将链表转化为红黑树,变为红黑树的目的是为了高效的查询
  • HashMap是线程不安全的,但由于整个类都是无锁结构,因此效率比较高

HashMap的常用常量和变量: 

1.序列化版本号常量:

    private static final long serialVersionUID = 362498820763181265L;

 2.集合的初始化容量(必须是2的n次幂):

   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

3.负载因子常量:

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

4.链表转红黑树时最小链表长度常量:

    static final int TREEIFY_THRESHOLD = 8;

5.链表转红黑树时最小数组长度常量:

    static final int MIN_TREEIFY_CAPACITY = 64;

6.红黑树转为链表的最小数组长度常量

    static final int UNTREEIFY_THRESHOLD = 6;

6.数组阈值(调整下一次扩容的容量值)(容量 * 负载因子)

    int threshold;

7.负载因子变量:

    final float loadFactor;

8.存储元素的数组:

    transient Node<K,V>[] table;

 9.存放具体元素的集合:

    transient Set<Map.Entry<K,V>> entrySet;

10.实际存储元素的个数:

    transient int size;

11. 记录HashMap的修改次数:

    transient int modCount;

HashMap的重要考点: 

HashMap的存储过程:

JDK1.8之前,HashMap由数组+链表 数据结构组成。

JDK1.8以后,HashMap由数组+链表+红黑树 数据结构组成。

而本文我们只讲讲解基于数组+链表+红黑树的HashMap。我们接下来用一个实例来讲解HashMap的存储过程。

public class Demo01 {public static void main(String[] args) {HashMap hashMap = new HashMap<String, Integer>();hashMap.put("a", 1);hashMap.put("b", 2);hashMap.put("c", 3);System.out.println(hashMap);}
}

当我们执行存储操作的时候,会发生如下操作:

  1. 计算哈希值:当调用put方法时,HashMap会将传入的key通过哈希函数(hash function)转换为一个整数,这个整数被称为哈希值。
  2. 计算数组索引:HashMap使用哈希值对数组的长度进行取模运算,得到一个数组索引(即数组的位置),用于确定键值对在数组中的存储位置。
  3. 存储链表或红黑树:如果该位置上已经有其他键值对存在,那么新的键值对将会以链表或红黑树的形式插入到该位置的末尾或中间。如果该位置还没有任何键值对,那么直接将键值对存储到该位置。

需要注意的是:在JDK8之前,HashMap的构造方法就会帮我们创建一个长度为16Entry[] table 用来存储键值数据。在JDK8以后就不是在HashMap的构造方法底层来创建数组了,而是在第一次调用put方法的时候创建一个Node[] table 来存储键值对数据。

好的,我们现在已经基本了解了HashMap是怎么插入一个数据的,接下来让我们看一下put方法的源码:

我们转到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))))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 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

小插入: 

我们对Key调用了哈希算法得到了一个int类型的整数,而这个哈希算法的底层逻辑是采用Key的HashCode方法的值结合数组长度进行无符号右移(>>),按位异或(^)

这个计算方式相对于直接使用hashCode()方法的好处在于,可以减少哈希冲突的概率。通过将二进制位的高位和低位都参与到哈希值的计算中,可以更好地保持哈希值的随机性和分布性,从而减少不同键的哈希冲突,提高HashMap的性能。

其实我们还有不少的方法同样可以得到一个hash值,比如取余。但位运算是硬件操作,可以直接在CPU的寄存器中进行操作,效率要高得多。因此我们一般都会使用位运算代替取余。而用位运算代替算术运算在后面还很常见,我们在这里要有这样的认识。

并且通过这个方法我们也可以看出:HashMap的Key是可以为null的。当Key为null的时候,将位置0作为键值对的插入位置。 

现在让我们现在正式的开始解析这段代码:

  • 首先,代码将table赋值给变量tab,并进行判空操作。如果table为空,则说明哈希表尚未初始化,此时将n设为0。
  • 如果table不为空,则将table的长度赋值给变量n。
  • 接下来,通过条件运算符判断n是否为0,如果是0,则表明哈希表的长度为0,需要进行扩容操作。在这种情况下,代码会调用resize()方法进行哈希表的扩容,并将扩容后的哈希表赋值给tab,并将扩容后的哈希表的长度赋值给n。
  • 最后,返回n作为哈希表的长度。

总的来说,这段代码的作用是获取哈希表的长度,并在需要扩容时进行相应的处理。通过检查哈希表的长度是否为0,可以判断哈希表是否需要进行初始化或者扩容,从而保证HashMap的正常使用和性能优化。

  • 首先,代码计算出要插入的位置i,通过对hash值进行(n-1) & hash的位与运算得到。这里的n是哈希表的长度,通常是2的幂,通过这个位与运算可以将hash值限定在0到n-1之间,确保落在哈希表范围内。
  • 然后,代码判断tab[i]位置是否为空,即当前位置是否已经存在节点。如果为空,则将新节点插入到这个位置。
  • 如果tab[i]位置不为空,则说明当前位置已经存在节点。这里可能会涉及到链表或者红黑树等数据结构,用于处理哈希冲突的情况,但在这段代码中没有展现出来。

总的来说,这段代码的作用是根据hash值找到对应的位置,然后判断该位置是否已经存在节点,如果不存在则直接插入新节点,如果存在则根据具体的情况进行相应的处理,以保证HashMap中的键值对能够正确存储和检索。

  • 首先,代码定义了一个Node和一个K类型的变量k。然后通过一系列的条件判断来确定如何处理哈希冲突:
    • 首先,判断当前节点p的哈希值和键与要插入的哈希值和键是否相等,如果相等,则将当前节点p赋值给e。
    • 如果不相等,且当前节点p是TreeNode类型,则调用TreeNode的putTreeVal方法来处理插入。
    • 如果以上两个条件都不满足,则通过遍历链表的方式找到合适的位置插入新节点,同时处理可能出现的树化操作。

总的来说,这段代码的作用是处理哈希冲突的情况,具体处理方式取决于当前节点的类型以及与要插入节点的哈希值和键的比较结果。通过合适的处理方式,保证HashMap在处理哈希冲突时能够正确地插入新节点,并维护数据结构的完整性。

  • 首先,将已存在节点e的值赋给oldValue,以便在后面返回旧的值。

  • 然后会根据onlyIfAbsent的取值来判断是否需要覆盖已存在节点e的值:

    • 如果onlyIfAbsent为false,或者oldValue为null(即允许替换已有的空值),则将新值value赋给节点e的值e.value。
  • 接着调用afterNodeAccess(e)方法来进行相关操作,比如LinkedHashMap中重写的方法会将节点移动到链表末尾,以实现LRU策略。

  • 最后返回旧的值oldValue,表示已存在键对应的旧值。

总的来说,这段代码的作用是在HashMap中处理键已存在的情况,根据需要更新节点的值,并在操作后返回旧的值。

  • 首先,对modCount进行自增操作,用于在并发情况下对HashMap的修改进行控制。

  • 然后对size进行自增操作,表示当前HashMap中键值对的数量增加了1。接着会检查size是否超过了阈值threshold,如果超过了则需要进行哈希表的扩容操作。

  • 如果size超过了threshold,就调用resize方法对哈希表进行扩容。哈希表的扩容会重新计算每个键值对的位置,以降低哈希冲突的概率。

  • 接着调用afterNodeInsertion(evict)方法来进行相关操作,其中evict参数表示是否需要进行LRU策略的节点移除操作。

  • 最后返回null,表示插入操作完成并且不需要返回任何值。

总的来说,这段代码的作用是在HashMap中处理插入新节点后更新相关计数器、进行哈希表的扩容操作,并在操作后进行相关处理。

 所以其实整个put操作的代码逻辑链其实是比较清晰的,我们可以用图表示为:

HashMap的扩容过程:

之所以要讲HashMap的扩容过程,是因为他的扩容和我们想的有点不一样,我们先看代码:

    final Node<K,V>[] resize() {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 thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = 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) {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 { // preserve orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}

按照我们的传统思路来讲:当原数组进行扩容之后,当我们想要把原数组中的数据放到新数组中的时候,基本的思路是对原数据进行重新Hash计算位置,放入数组。但这样的效率太低了。HashMap的扩容并没有采用这种机制。

我们先来看源代码中HashMap的扩容是如何做的:

简而言之:HashMap认为:旧节点的新位置要么在原位置(newTab[j] = loHead),要么在原位置+旧容量(newTab[j + oldCap] = hiHead)上 

 我们来看一下为什么可以这么认为:

当我们进行扩容的时候,每次的大小都是翻倍的(2的n次幂),例如从4到8,16到32。这种内存翻倍后与原来计算(n-1)& hash 的结果相比,只是多了一个bit位。

例如:当我们从4 扩容的 8 的时候:

​​​n:

4:0100        16:010000

--------------------------------------------------------------------------------------------------------------------------

8:1000        32:100000

n-1

3:0011        15:001111

--------------------------------------------------------------------------------------------------------------------------

8:0111        32:011111

也就是说实际上扩容后,从二进制来看,运算结果只有最高位发生了变化,如果元素的二进制最高位为0,那么就是原位置,如果二进制最高是1,那么就会变为1,也就是原位置+旧容量

正是因为这种巧妙的rehash的方式,大大提高了运行效率。

HashMap的初始化:

我们不讲HashMap的自动初始化,源码比较简单,大家可以自己看一看。而在这里要着重讲一下HashMap的手动扩容过程:

我们都知道:HashMap的扩容过程是一个比较浪费时间的过程。因此我们如果想要提高代码运行的效率,就要手动设置初始大小,避免自动扩容。但有的人会在这里陷入一个误区,我们通过一个实际问题来引出:

当我们要存放七个元素的时候,我们应该手动初始化大小为多少呢?

 很多同学肯定下意识的回答 8 。 但其实是错误的!让我们来思考一下:

如果设置为8 的话, 负载系数默认是0.75。那么 8 * 0.75 =6,也就是说数组中实际元素达到6个就要扩容,我们如果存放七个元素,手动设置大小为8 的话,还是避免不了扩容。 

因此:我们应该 手动设置为(需要存储的元素个数/负载因子)+1。这是计算手动设置容量的通用方法。

注意:不可以修改负载系数,0.75是官方大量数据统计出来的一个比较均衡的负载因子,我们基本不会对其做修改!

常见面试题:

1.为什么集合的初始化容量必须是2的n次幂?

首先,当我们尝试向HashMap中添加一个元素的时候,需要根据Key的hash值得到数组中的具体位置。而我们不可以直接使用Key的hash值。这是因为Key的hash值不一定就在数组下标范围内,因此我们要对Key的hash值再次进行操作,使其满足值的范围在数组下标范围内

这里的第一个思路就是取余。我们让hash%n(数组长度),这样就可以控制得到的值始终在数组下标范围内。

但是这里又有一个问题:如果是取余的话,效率是很低的,不如使用位运算。而我们的代码在实际中也是用位运算来代替取余操作

而我们在实际中也是这么做的,但是hash % n 和 (n - 1)& hash 的值相等的前提是 n 是2 的 x 次幂

 而除此之外,返回到使用这个方法的最外层,我们的目的还是为了让数据能够均匀分布,减少Hash冲突。

如果创建HashMap的时候,输入的数组长度不是2的幂,那么HashMap就会通过位运算得到一个比我们输入的数组长度大的,离我们输入数组长度最近的一个2的幂。

2.hash方法为什么要右移16位异或?((h = key.hashCode()) ^ (h >>> 16))

其中 h = key.hashCode() ^ (h >>> 16) 的目的是为了增加哈希值的随机性,使得节点在哈希表中分布更均匀,减少哈希冲突,提高查找效率。

具体来说:

  • 首先,通过 key.hashCode() 获取键的哈希码。
  • 接着,通过 h >>> 16 将 h 右移16位,然后将结果与 h 进行异或操作 ^。这样做是为了让高位的信息参与到低位的计算中,增加低位的随机性。

通过这样的运算,可以让原始的哈希值的高位和低位进行混合,并且引入了一定程度的随机性,使得最终的哈希值分布更加均匀,减少了哈希冲突的概率。这种处理方式有助于提高HashMap的性能,特别是在处理大量数据时,能够更有效地分散数据,减少链表长度,提高查找效率。

如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题。

3.负载因子的作用是什么

负载因子用来判断当前数组的负载程度,简单来讲就是有多少元素。当数组中的实际存在元素个数大于 数组长度 * 负载因子的 时候,就会进行扩容操作。 这点我们可以在put代码中看到:

我们可以看到当数组size大于threshold的时候,就进行扩容操作。我们可以在resize方法中看到thresould的计算方法:

 4.HashMap如何保证初始化数组长度始终是2的n次方的

 我们可以看到默认的带参构造函数对我们输入的数据进行了这样的操作,具体方法如下:

 static final int tableSizeFor(int cap) {int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

而这个方法中又调用了 Integer.numberOfLeadingZeros 方法,我们来看看:

 numberOfLeadingZeros方法用来计算32位长度的二进制数字的前导零:

例如 9 的二进制:

00000000 00000000 00000000 00001001

那么9的前导零就是28,我们来看看代码执行结果:

好的,让我们回到  tableSizeFor   方法中 :

 static final int tableSizeFor(int cap) {int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

我们来模拟一下tableSizeFor操作:

  1. 输入一个数字 5。
  2. numberOfLeadingZeros方法 对(5-1)得到结果为:29。
  3. n等于-1>>>29,结果为7。
  4. 进入判断: n>0 则执行(n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1。
  5. n< MAXIMUM_CAPACITY(1<<30),则返回n+1,也就是7+1 =7

这样,我们就得到了一个比5大的,距离5最小的一个二次幂。这就是tablesizeFor的运行原理。

需要注意的是这里返回的结果并没有直接赋值给HashMap的初始实例数组长度,而是给了扩容阈值 threshould !!!‘

而真正的初始化实际上发生在第一次调用put方法插入元素的时候:

总体的逻辑为:当旧数组为空,旧阈值不为空的时候,把旧阈值赋值给新数组。 

就这样,我们就实现了带参数的HashMap实例创建。

5.HashMap将链表转化为红黑树的链表边界长度为什么是8

这个问题其实在HashMap的源码中就给了解释:

简要的意思就是:由于树节点的大小是链表节点的两倍,因此我们轻易不会将链表转化为红黑树,这会对内存造成浪费。而在大量的实际数据插入的情况下,我们统计发现:箱子中的节点的频率服从泊松分布。

当连续插到链表的第八个节点的时候,实际上概率已经很小了,已经达到了0.00000006。也就是说我们将链表的长度设置为8,就已经可以应对大多数的HashMap的Hash碰撞了 。当节点大于8的时候,我们再考虑查询的效率问题,将其转化为红黑树。

总结来讲,将链表转换红黑树的链表长度边界设置为8,是综合时间和空间的结果。

总结:

综上所述,本文深入介绍了HashMap的底层源码实现原理。首先,我们深入探讨了HashMap的数据结构,它基于数组和链表(或红黑树)实现,这使得HashMap能够高效地存储键值对。其次,我们详细分析了HashMap的哈希算法,通过hashCode方法和位运算来确定元素在数组中的位置,实现了快速的查找、插入和删除操作。对于扩容与负载因子的讨论使我们更好地理解了HashMap是如何动态调整大小以保持性能的。

通过本文的介绍,读者不仅可以深入了解HashMap的具体实现细节,还能够从中学习到在实际开发中如何更好地利用HashMap的特性。深入理解HashMap的底层源码将有助于提升对Java集合框架的整体理解,并能够在日常开发中更加灵活、高效地应用HashMap这一重要的数据结构。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI摄影绘画与PS优化:重塑数字艺术的未来

文章目录 《AI摄影绘画与PS优化从入门到精通》内容简介作者简介楚天 目录前言/序言 在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的各个领域&#xff0c;包括艺术创作。AI摄影绘画和Photoshop&#xff08;PS&#xff09;优化是这个领…

「Verilog学习笔记」交通灯

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 timescale 1ns/1nsmodule triffic_light(input rst_n, //异位复位信号&#xff0c;低电平有效input clk, //时钟信号input pass_request,output wire[7:0]clock,output reg…

使用Alpha Vantage API和Python进行金融数据分析

Alpha Vantage通过一套强大且开发者友好的数据API和电子表格&#xff0c;提供实时和历史的金融市场数据。从传统资产类别&#xff08;例如股票、ETF、共同基金&#xff09;到经济指标&#xff0c;从外汇汇率到大宗商品&#xff0c;从基本数据到技术指标&#xff0c;Alpha Vanta…

扫雪铲冰,绿萝护航平安出行

为保护行人出行安全&#xff0c;确保道路干净整洁&#xff0c;绿萝党员志愿者带头行动&#xff0c;号召志愿者积极参与扫雪铲冰。2023年12月16日&#xff0c;绿萝志愿服务队与拱辰集体林场携手在昊天碳中和公园开展了志愿扫雪活动。 行动是最好的融雪剂。大雪过后的道路是最为寒…

自动驾驶TPM技术杂谈 ———— 交通事件分类与编码

文章目录 介绍交通事件分类交通事件分类顺序码交通事故交通灾害交通气象路面状况道路施工活动重大事件其他 介绍 1. 交通事件 —— 由于人、车辆、设施、环境之间的不协调导致正常交通秩序的突发性混乱事件。  2. 交通管制 —— 公安机关交通管理部门根据法律、法规&#xff0…

2.1 数据操作

声明&#xff1a; 注意&#xff01;&#xff01;&#xff01; 本博客仅用于本人学习笔记作用&#xff0c;所有资料来源都来自于李沐大神&#xff0c;博客中图片为沐神书上的图片。如有侵权&#xff0c;请联系本人删除&#xff0c;谢谢。 资料来源&#xff1a;https://zh-v2.d2…

什么是通讯芯片?通讯芯片又分什么种类?

通讯芯片是指在通讯系统中起到连接和传输信号的重要组成部分。根据通讯芯片的功能和应用,可以将其分为以下几类: 1、收发器芯片 2、窄带中频放大器芯片 3、卫星接收器LNB芯片 4、电话机芯片 5、压缩扩展器芯片 以上是通讯芯片的一些分类&#xff0c;随着通讯技术的不断发展…

patchless amsi学习(中)

DR7 DR7被称为“调试控制寄存器”&#xff0c;允许对每个硬件断点进行精细控制。其中&#xff0c;前8位控制是否启用了特定的硬件断点。偶数位&#xff08;0、2、4、6&#xff09;称为L0-L3&#xff0c;在本地启用了断点&#xff0c;这意味着仅在当前任务中检测到断点异常时才…

接口自动化测试实战教程

实现思路 使用excel管理用例用例信息&#xff0c;requests模块发送http请求&#xff0c;实现了记录日志&#xff0c;邮件发送测试报告的功能 目录结构如下&#xff1a; 下面直接上代码&#xff1a; 统筹脚本 # -*- coding:utf-8 -*-import osfrom interface import Interfa…

Android Termux安装MySQL数据库并通过内网穿透实现公网远程访问

文章目录 前言1.安装MariaDB2.安装cpolar内网穿透工具3. 创建安全隧道映射mysql4. 公网远程连接5. 固定远程连接地址 前言 Android作为移动设备&#xff0c;尽管最初并非设计为服务器&#xff0c;但是随着技术的进步我们可以将Android配置为生产力工具&#xff0c;变成一个随身…

【MongoDB】--MongoDB的Sort排序问题

目录 一、问题背景描述1.1、问题背景1.2、问题分析 二、建立索引支持深度翻页查询2.1、调整sort排序的内存限制【不建议】2.2、创建索引2.3、拓展--组合索引什么时候失效 二、聚合查询解决深度翻页查询 一、问题背景描述 1.1、问题背景 现实系统页面翻页到20000页之后&#x…

Improving IP Geolocation with Target-Centric IP Graph (Student Abstract)

ABSTRACT 准确的IP地理定位对于位置感知的应用程序是必不可少的。虽然基于以路由器为中心(router-centric )的IP图的最新进展被认为是前沿的,但一个挑战仍然存在:稀疏IP图的流行(14.24%,少于10个节点,9.73%孤立)限制了图的学习。为了缓解这个问题,我们将目标主机(ta…

【LeetCode刷题笔记(8-2)】【Python】【接雨水】【单调栈】【困难】

文章目录 引言接雨水题目描述提示 解决方案2&#xff1a;【单调栈】结束语 【接雨水】 【LeetCode刷题笔记&#xff08;8-1&#xff09;】【Python】【接雨水】【动态规划】【困难】 引言 编写通过所有测试案例的代码并不简单&#xff0c;通常需要深思熟虑和理性分析。虽然这…

200kw直流电子负载测试

在进行200kw直流电子负载测试时&#xff0c;需要设置合适的负载参数&#xff0c;如电流、电压和功率等&#xff0c;并记录测试结果。在测试过程中&#xff0c;可以通过改变负载参数来模拟不同工作条件下的电子设备的性能。通过对电子设备在不同负载条件下的响应进行测试和分析&…

JupyterHub 如何切换 conda 小环境

JupyterHub 如何切换 conda 小环境 服务器已经部署好 JupyterHub &#xff0c;相关端口请看对应答疑群群公告。在Jupyterhub 中使用 conda 创建的小环境&#xff0c;首先 ssh 登录上服务器或者在 JupyterHub 网页端打开终端 terminal。然后安装 conda &#xff0c;方法请见 Q4&…

【MyBatis Plus】逻辑删除、分页、乐观锁的应用及讲解

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《MyBatis-Plus》。&#x1f3af;&#x1f3af; &am…

改进灰狼算法求解:考虑需求响应的风-光柴-储容量优化配置

目录 文章摘要&#xff1a; 亮点&#xff1a; 研究背景&#xff1a; 考虑需求相应的容量配置&#xff1a; 风、光、柴、储微电网模型&#xff1a; 储能配置模型&#xff1a; 改进的灰狼算法&#xff1a; 基于余弦规律变化的收敛因子 引入动态权重策略 运行效果&#…

数据结构:图文详解 队列 | 循环队列 的各种操作(出队,入队,获取队列元素,判断队列状态)

目录 队列的概念 队列的数据结构 队列的实现 入队 出队 获取队头元素 获取队列长度 循环队列的概念 循环队列的数据结构 循环队列的实现 判断队列是否为空 判断队列是否已满 入队 出队 得到队头元素 得到队尾元素 队列的概念 队列&#xff08;Queue&#xff0…

基于单片机的视力保护及身姿矫正器设计(论文+源码)

1. 系统设计 在本次设计中&#xff0c;其系统整个框图如图2-1所示。其主要的核心控制模块由超声波模块&#xff0c;光敏电阻&#xff0c;按键模块&#xff0c;复位电路&#xff0c;红外模块&#xff0c;LCD显示等组成。其包括自动模式&#xff0c;手动模式。自动模式&#xff…

upload-labs笔记

简介 upload-labs是一个使用php语言编写的&#xff0c;专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共21关&#xff0c;每一关都包含着不同上传方式。 文件上传漏洞是指&#xff1a; Web 服务器允许用户将文件上传至其…