由浅入深了解HashMap源码

news2024/12/22 23:37:30

       由经典面试题引入,讲解一下HashMap的底层数据结构?这个面试题你当然可以只答,HashMap底层的数据结构是由(数组+链表+红黑树)实现的,但是显然面试官不太满意这个答案,毕竟这里有一个坑需要你去填,那就是在回答HashMap的底层数据结构时需要考虑JDK的版本,因为在JDK8中相较于之前的版本做了一些改进,不仅仅是增加了红黑树的数据结构、还包括了链表结点的插入由头插法改成了尾插法,这些都是底层数据结构的优化问题。

 JDK8中HashMap的数据结构

        从上面数据结构原理图中我们能看出数组和链表是如何组合使用的,数组不是实际保存数据的结构,数组保存的是Node<K,V>的对象引用地址,实际保存数据的是Node<K,V>结点类。数组中的每个位置只能保存一个Node<K,V>对象,通过链表可以在同一位置保存多个数据,还有链表会在一定条件下转化为红黑树。

  • table数组
// 用于保存 Node<K,V> 类型的数组
transient Node<K,V>[] table;
  •  HashMap的默认初始化容量,指的是table数组大小

 // HashMap的默认初始化容量为16,1位运算左移4位
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
  •  HashMap的默认负载因子,用于计算阈值
// HashMap的负载因子用于计算阈值,超过阈值即负载过大需要数组扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  •  HashMap的阈值,当HashMap中的元素个数超过阈值时,数组会扩容为原大小的2倍
 // 扩容的阈值(默认阈值 = 默认数组容量 * 负载因子),默认为12 = 16 * 0.75
 int threshold;
  •  HashMap的链表转换为红黑树的阈值
// 树化的阈值,不是唯一条件,而是必须条件
static final int TREEIFY_THRESHOLD = 8;
  • Node<K,V>结点类
 // HashMap源码的静态内部类
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    // 保存key计算出的hash码
        final K key;       // 保存key的值
        V value;           // 保存value的值
        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;
        }

 HashMap的构造方法

  • HashMap的无参构造
// 此时只设置了默认的负载因子,即数组未初始化
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
 }
  • HashMap的带负载因子和数组容量的构造方法
 // 可设置自定义负载因子和数组容量
public HashMap(int initialCapacity, float loadFactor) {
     // 数组容量不能小于0
     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
     // 数组容量不能大于MAXIMUM_CAPACITY
     if (initialCapacity > MAXIMUM_CAPACITY)
         initialCapacity = MAXIMUM_CAPACITY;
     // 负载因子不能小于等于0
     if (loadFactor <= 0 || Float.isNaN(loadFactor))
         throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
     this.loadFactor = loadFactor;
     // 用于计算table数组的最终大小,因为数组大小必需为2的n次方数
     this.threshold = tableSizeFor(initialCapacity);
}
  • HashMap的可传入Map集合数据的构造方法
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 遍历Map取出集合的数据依次放入HashMap中
    putMapEntries(m, false);
}

 HashMap的put方法

  •  HashMap的put方法,实际调用了putVal方法
public V put(K key, V value) {
    // 计算key的哈希值,创建Node<K,V>结点放入数组中
    return putVal(hash(key), key, value, false, true);
}
  •  HashMap的hash方法返回的哈希值,并不是直接调用Object对象的hashCode()方法返回的那个哈希值,而是经过了异或运算后的哈希值。
// 计算key的哈希值的方法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  •  异或运算就是两个二进制数值按位比较,同位的值相同则为0,同位的值不同则为1。我们分析一下,先计算出key的哈希值用作第一个数,再把key的哈希值右移16位作为第二个数,然后再把两个数值进行异或运算(^)。这样计算出的哈希值会变得更随机,因为增加了高16位参与,能降低哈希冲突的概率。
// 425918570 对应的32位的二进制哈希值 

// 高16位                    低16位
0001 1001 0110 0011 --- 0000 0000 0110 1010     // 原始哈希值
0000 0000 0000 0000 --- 0001 1001 0110 0011     // 原始哈希值右移16位的值     
0001 1001 0110 0011 --- 0001 1001 0000 1001     // 异或运算得到的哈希值
  • putVal方法是添加数据的核心方法,table数组会在第一次添加元素时进行初始化,默认初始化容量为16,在添加数据时需要通过哈希值计算出数据放入的table数组所在的索引下标位置。添加完元素后需要判断数组是否需要扩容,扩容大小是原数组的两倍。
 // HashMap添加数据的核心方法,
 // onlyIfAbsent = false表示key相等时会覆盖旧value
 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数组是第一次使用时才进行初始化,懒惰使用
        if ((tab = table) == null || (n = tab.length) == 0)
            // resize()包括了数组的初始化和扩容
            n = (tab = resize()).length;
        // table数组当前计算出的下标位置还未保存过元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 创建新结点,直接放入计算出的数组下标位置中
            tab[i] = newNode(hash, key, value, null);
         else {
            Node<K,V> e; K k;
            // 出现哈希冲突,需要判断是否是相同key,因为不同key也可能有相同的哈希值
            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) {
                        // 先插入结点,再判断阈值,故链表长度大于8时才是树化必须条件
                        p.next = newNode(hash, key, value, null);
                        // 链表的长度大于8时,走树化方法
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果当前遍历到的链表结点和需要添加的数据key相同,则无需插入,直接退出
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 添加结点数据时发现key已经存在,此时不是插入而是更新
            if (e != null) { 
                V oldValue = e.value;
                 // put相同key的数据时会覆盖旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                // 返回当前key的旧值
                return oldValue;
            }
        }
        ++modCount;
        // HashMap中元素个数大于阈值,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);   
        return null;
}
  • putVal方法中添加Node<K,V>的结点元素时,如何计算元素所在的table数组下标位置呢?这里就需要用到我们的哈希值了,我们可以利用哈希值对数组的最大索引下标值进行取模运算,这样就可以把取模运算的结果当做数组的下标位置。但是实际源码中并不是这样做的,而是采用了与运算(&),利用哈希值和数组的最大索引下标进行按位与运算。
// 32位的二进制哈希值

// 高16位                    低16位
0001 1001 0110 0011 --- 0001 1001 0000 1001   // 最终计算出的哈希值
0000 0000 0000 0000 --- 0000 0000 0000 1111   // table数组的最大索引下标,使用默认大小就是15
0000 0000 0000 0000 --- 0000 0000 0000 1001   // 与运算(&)的结果 9,即下标位置

HashMap的数组长度为2的n次方数的原因

  • 面试经常会问HashMap的数组长度为什么强制使用2的幂次方数?要回答这个问题,那么我们就需要知道数组长度在哪个地方用到了,通过源码我们发现数组的长度被用于添加元素时计算数组的下标位置。数组下标是通过与运算(&)计算出来的,与运算的特点是(全1为1,有0为0),而2的幂次方数减一正好保证后面全为1,这样就可以使与运算计算的结果降低相同值的概率,本质就是减少哈希冲突的概率。
// 数组下标计算公式
int i = (n - 1) & hash

// 当hash值为10,数组长度n为9时
// 高16位                    低16位
0000 0000 0000 0000 --- 0000 0000 0000 1010   // 影响&运算的有效位为1位,容易产生哈希冲突
0000 0000 0000 0000 --- 0000 0000 0000 1000   // (n - 1)的值为8
0000 0000 0000 0000 --- 0000 0000 0000 1000   // 计算出的数组下标为8

// 当hash值为10,数组长度n为16时
// 高16位                    低16位
0000 0000 0000 0000 --- 0000 0000 0000 1010   // 影响&运算结果的有效位为4位,不易产生哈希冲突
0000 0000 0000 0000 --- 0000 0000 0000 1111   // (n - 1)的值为15
0000 0000 0000 0000 --- 0000 0000 0000 1010   // 计算出的数组下标为10

  • 数组扩容后Node<K,V>元素的存放的数组下标位置变化不大,只有两种可能,不是在新数组的原索引值位置,就是在原索引值+原数组长度的位置。我们知道数组扩容是在原数组容量的基础上乘以2,根据原理可推断,在重新计算元素保存的数组下标位置时,(n - 1)带来的影响很小,只会增加一个有效位的计算。即扩容前(n - 1) = 15是4个1,扩容后(n - 1) = 31是5个1。
// 数组下标计算公式
int i = (n - 1) & hash
 
// 当hash值为10,扩容前,数组长度n为16
// 高16位                    低16位
0000 0000 0000 0000 --- 0000 0000 0000 1010   // &运算结果的有效位为4位
0000 0000 0000 0000 --- 0000 0000 0000 1111   // (n - 1)的值为15
0000 0000 0000 0000 --- 0000 0000 0000 1010   // 计算出的数组下标为10
 
// 当hash值为10,扩容后,数组长度n为32
// 高16位                    低16位
0000 0000 0000 0000 --- 0000 0000 0000 1010   // &运算结果的有效位为5位,增加一位有效位计算
0000 0000 0000 0000 --- 0000 0000 0001 1111   // (n - 1)的值为31
0000 0000 0000 0000 --- 0000 0000 0000 1010   // 计算出的数组下标还是10

HashMap的链表树化的条件

  •  链表什么时候树化,我们可以查看在添加结点时判断是否需要树化的源码逻辑。
// 树化的阈值,不是唯一条件,而是必须条件
static final int TREEIFY_THRESHOLD = 8;

 // 当遍历的结点数大于等于8时
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
     // 进入是否需要树化的方法
     treeifyBin(tab, hash);
  •   从源码中我们可以了解到链表转换为红黑树需要满足两个条件:一是数组的容量需要大于64二是链表的长度要大于阈值8。当必要条件链表的长度要大于8时,才会选择优化缩短链表长度,此时不一定就需要直接转换为红黑树,还可以通过扩容数组的方式来使链表长度变短,所以当数组的容量小于64时,是采取数组扩容来优化链表的。
// 将链表结点转换为红黑树,如果数组容量太小则先扩容数组
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 数组为空或容量小于64,扩容数组
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 找到当前链表的头结点
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            // 遍历普通结点链表转换为树结点的双向链表
            do {
                // 链表结点替换成树结点返回
                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);
            // 将数组中的普通结点链表替换成树结点的双向链表
            if ((tab[index] = hd) != null)
                // 构造生成红黑树
                hd.treeify(tab);
        }
 }

 JDK8的HashMap为什么在链表中使用尾插法代替了头插法

  • 链表结点的头插法,就是在链表的头部插入数据,就是每次插入的新结点数据都会成为链表的头结点。JDK7的HashMap中是否真的使用了头插法,我们可以从源码中求证。
 // 用于添加Entry<K,V>结点的方法,jdk1.7叫Entry
void createEntry(int hash, K key, V value, int bucketIndex) {

    // table数组保存的头结点Entry保存到e变量
    Entry<K,V> e = table[bucketIndex];
    
    // 1. 把e元素作为新结点的next结点,即原头结点作为新结点的下一个结点
    // 2. 新结点作为头结点保存到table[bucketIndex],即头插法
    table[bucketIndex] = new Entry<>(hash, key, value, e);

    // HashMap中保存的元素总数+1
    size++;

}
  • 链表结点的尾插法,就是在链表的尾部插入数据,这样就需要遍历链表找到尾结点,使尾结点的Node<K,V> next结点指向新结点。JDK8的HashMap添加结点使用尾插法的源码实现就是如此,前面在源码中已经看到过。
 // 从数组下标所在的头结点开始遍历链表
for (int binCount = 0; ; ++binCount) {
      // 找到尾结点,在尾结点后插入新结点,即尾插法
      if ((e = p.next) == null) {
           // 新结点作为尾结点的next结点,并成为新尾结点
           p.next = newNode(hash, key, value, null);
          // 链表的长度大于8时,走树化方法
          if (binCount >= TREEIFY_THRESHOLD - 1) 
              treeifyBin(tab, hash);
          break;
      }
      // 如果当前遍历到的链表结点和需要添加的数据key相同,则无需插入,直接退出
      if (e.hash == hash &&
             ((k = e.key) == key || (key != null && key.equals(k))))
           break;
      p = e;
}
  •  了解了头插法和尾插法的区别,那么JDK8中的HashMap为什么抛弃了头插法而使用尾插法呢?这是因为头插法在数组扩容时,链表重新在新数组中生成时会导致链表元素倒序,这里比较难理解。因为链表的遍历是从头结点开始的,而链表插入是头插法,会一直更新头结点;所以就是最早插入的结点成为尾结点,最后插入的结点成为头结点,这是头插法的特点。这里的链表元素倒序表面上看起来问题不大,但是在多线程同时操作一个HashMap的情况下容易产生死链问题,这才是根本的。

 

  •   JDK7中的HashMap在数组扩容时为什么会死链,当然这是并发问题,是可能出现,不是说一定就会死链。我们要结合扩容的源码进行分析,不一定要非常深入理解,能找到问题所在就行。
// jdk1.7的扩容过程
void transfer(Entry[] newTable, boolean rehash) {
     // 获取新数组的容量
     int newCapacity = newTable.length;
     // 遍历旧数组,获取头结点,单节点也是链表
     for (Entry<K,V> e : table) {
          // 遍历链表
          while(null != e) {
              // 取出当前结点的next结点
             Entry<K,V> next = e.next;
              // 判断是否需要重新计算hash值
             if (rehash) {
                  e.hash = null == e.key ? 0 : hash(e.key);
             }
             // 因为数组扩容了,重新计算元素存放的数组位置
             int i = indexFor(e.hash, newCapacity);

              // 当前添加的结点e作为头结点,所以它的e.next指向旧的头结点
             e.next = newTable[i];
             // 最新添加的结点成为新数组保存的头结点,每次更新头结点,即头插法
             newTable[i] = e;
               
             // 遍历的写法,取下一个节点,
             e = next;    
          }
     }
}
  •  分析Entry[ ] newTable作为外部传入的引用变量存在线程安全问题,Entry[ ] table不是方法的局部变量也存在线程安全问题。我们通过分析知道,头插法会导致链表结点元素倒序,即从A > B > C变为C > B > A,这就出现了大问题。在多线程环境下,如果一个线程完成了扩容,Entry[ ] table会引用新数组Entry[ ] newTable,由于链表结点引用指向发生了倒序,那么另一个线程就会产生死链,在遍历中产生死链会陷入死循环。
// 假设扩容前当前遍历的链表为: A > B > C
// 分析t1线程发生死链的情况,有两个线程 t1 和 t2,都在执行扩容操作
 while(null != e) {
     
      // t2线程未扩容成功时,t1线程执行,当 e = B,e.next一定为 C
      Entry<K,V> next = e.next;

      // t1线程发生上下文切换,此时t2线程先完成了扩容
      // 由于链表倒序为:C > B > A,当 e = B时,e.next的引用变为了 A, next还是引用 C
      if (rehash) {
          e.hash = null == e.key ? 0 : hash(e.key);
      }
      int i = indexFor(e.hash, newCapacity);
      // B结点指向头结点C,即 B > c > B,发生死链
      e.next = newTable[i];
      // B结点变为头结点
      newTable[i] = e;
   
     // 继续遍历,下一个节点变为C,由于死链会变为死循环
     e = next;    
}

JDK8的HashMap为什么引入了红黑树的数据结构

  •  红黑树是一种自平衡二叉搜索树,并不是完全平衡的二叉树。完全平衡二叉树需要自旋多次才能达到平衡,而红黑树不需要多次自旋,同样有很好的查询性能,缺点是引入了结点颜色维护,变得更加复杂。

  • 《算法导论》中对于红黑树的定义:①每个结点不是红结点就是黑结点 ;②根结点是黑色的; ③每个叶子节点(NIL)都是黑的;④如果一个节点是红的,那么它的两个儿子都是黑的;⑤对于任意一个结点,其到叶子节点(NIL)的所有路径上的黑结点数都是相等的。HashMap中使用红黑树的目的是为了提升查询效率,因为链表过长,会导致查询效率变低。HashMap中的链表会在数组容量大于64和链表长度大于8时转换为红黑树,同时红黑树也会在结点数小于等于6时退化为链表。 

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

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

相关文章

Java容器使用注意点

前置&#xff1a;问题 判空集合转map集合遍历集合去重集合转数组数组转集合 一&#xff1a;集合判空 《阿里巴巴 Java 开发手册》的描述如下&#xff1a; 判断所有集合内部的元素是否为空&#xff0c;使用 isEmpty() 方法&#xff0c;而不是 size()0 的方式。 我们在开发中也…

AI大模型突围战

最近&#xff0c;GPT赛道上争议不断&#xff0c;先是GPT-4的问世&#xff0c;用一些亮眼的功能让人类感叹自己是不是要丢工作&#xff1f;紧接着又有一封联名信刷屏&#xff0c;图灵奖得主、AI三巨头之一的约书亚本吉奥、埃隆马斯克等123个业界大佬&#xff0c;呼吁所有人工智能…

Vcpkg安装指定版本包或自定义安装包

文章目录前言寻找版本安装后话前言 windows一直用着vcpkg作为C跨平台开发的包管理&#xff0c;有些依赖要指定版本库&#xff0c;vcpkg目前最新的openssl版本是3.1.0&#xff0c;我想安装其他版本为例&#xff0c;开始本教程 寻找版本 以openssl举例&#xff0c;在vcpkg目录中…

react3:受控组件(表单默认变成受控)-组件通信 - typescript项目

受控组件&#xff1a;表单 非受控组件表单元素值不受所在组件状态的控制, 我们将这样的表单元素称作: 非受控组件. 受控组件受控组件 : 值受到 React 组件状态控制的表单元素一般是通过 defaultValue 属性, onChange 事件配合将非受控组件变为受控组件. 多表单元素操作 &#…

如果当前node.js版本和项目需要版本不一样,卸载重装其他版本node.js的方法

其实这种node.js版本不一样的问题&#xff0c;可以选择用nvm来管理node.js的不同版本。 此处仅总结卸载当前版本node.js重新安装所需版本node.js的方法 另&#xff1a;现在 用Vite官网里面的 yarn/npm等 的方法&#xff0c;创建Vue3项目——需要12版本的node.js 以下以本人 …

Vue3中readonly 与 shallowReadonly的使用区别?

文章目录前言readonly强行修改readonly&#xff1a;shallowReadonlyshallowReadonly强行修改结果&#xff1a;前言 readonly: 让一个响应式数据变为只读的&#xff08;深只读&#xff09;。 shallowReadonly&#xff1a; 让一个响应式数据变为只读的&#xff08;浅只读&#x…

真正好用的工业品ERP系统应该是什么样的?

一个好用的进销存ERP系统应该有以下特点&#xff1a; 1. 全面覆盖企业经营流程&#xff0c;包括采购、销售、库存、财务等模块&#xff0c;能够实现全方位的管理和控制。 2. 自定义配置&#xff0c;灵活地适应大多数用户的需求。 3. 数据精准、实时化&#xff0c;支持统计分…

基于CALMET诊断模型的高时空分辨率精细化风场模拟技术应用

【查看原文】气象数据相关分析及使用系列&#xff1a;基于CALMET诊断模型的高时空分辨率精细化风场模拟技术应用 在研究流场时&#xff0c;常用观测、模型风洞测试和数值模拟方法进行研究。但时常遇到研究区气象站点分布稀疏&#xff0c;不能代表周边复杂地形的风场。风洞模拟…

day7 线程的取消和清理

线程的取消 意义&#xff1a;随时杀掉一个线程 int pthread_cancel(pthread_t thread); 注意&#xff1a;线程的取消要有取消点才可以&#xff0c;不是说取消就取消&#xff0c;线程的取消点主要是阻塞的系统调用 如果没有取消点&#xff0c;手动设置一个&#xff1b; voi…

【网络应用开发】实验4——会话管理

目录 会话管理预习报告 一、实验目的 二、实验原理 三、实验预习内容 1. 什么是会话&#xff0c;一个会话的生产周期从什么时候&#xff0c;到什么时候结束&#xff1f; 2. 服务器是如何识别管理属于某一个特定客户的会话的&#xff1f; 3. 什么是Cookie&#xff0c;它的…

全网最详细,Python接口自动化测试接口加密实战,框架撸码.......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 如果接口测试仅仅只…

【驱动开发】Windows过滤平台(WFP,Windows Filtering Platform)

文章目录Windows的发展历程TDI简介WFP简介用户态基础过滤引擎&#xff08;BFE&#xff09;内核态过滤引擎&#xff08;KMFE&#xff09;垫片&#xff08;Shim&#xff09;分层&#xff08;Layer&#xff09;子层&#xff08;Sub Layer&#xff09;过滤器&#xff08;Filter&…

【微信小程序-原生开发】实用教程20 - 生成海报(实战范例为生成活动海报,内含生成指定页面的小程序二维码,保存图片到手机,canvas 系列教程)

可在系列教程的基础上继续开发&#xff0c;也可以单独使用 【微信小程序-原生开发】系列教程 效果预览 代码实现 点击触发生成海报 在活动详情页&#xff0c;指定点击某图标/按钮&#xff0c;触发跳转到生成海报的页面 pages\components\party\detail\index.js getPoster() …

OpenTex 企业内容管理平台

OpenText 企业内容管理平台 将内容服务与领先应用程序集成&#xff0c;弥合内容孤岛、加快信息流并扩大治理 什么是内容服务集成&#xff1f; 内容服务集成通过将内容管理平台与处于流程核心的独立应用程序和系统连接起来&#xff0c;支持并扩展了 ECM 的传统优势。 最好的内…

【通过Cpython3.9源码看看python字符串对象的创建】

CPython源码解析之PyUnicode_New函数实现 简介 PyUnicode_New是Python源码中用于创建Unicode字符串对象的函数&#xff0c;定义在UnicodeObject.c文件中。该函数接受一个长度参数size和最大字符值参数maxchar(根据传入的最大字符值 maxchar 确定新创建的字符串对象所需的存储…

百度工程师的软件质量与测试随笔

作者 | 百度移动生态质效工程师们 导读 在降本增效、以chatGPT为代表的大模型技术横空出世的背景下&#xff0c;对软件质量和软件测试的领域也带来了巨大冲击&#xff0c;也使得软件质量工作者开始变得焦虑&#xff0c;主要体现在&#xff1a;公司对软件质量从业者的不重视加剧…

SQL Server的执行计划(Execution Plans)

执行计划一、背景二、显示和保存执行计划三、显示估计的执行计划四、显示实际执行计划五、以 XML 格式保存执行计划六、比较和分析执行计划6.1、比较执行计划6.2、分析实际执行计划总结一、背景 为了能够执行查询&#xff0c;SQL Server 数据库引擎必须分析该语句&#xff0c;…

21100颗星的Locust性能测试工具到底有多牛!

一句话&#xff1a;用普通的Python编写可扩展的负载测试&#xff0c;就够了&#xff0c;懂得自然懂&#xff01; Locust是一个易于使用、可编写脚本和可扩展的性能测试工具。你在常规的Python代码中定义你的用户的行为&#xff0c;而不是受制于一个UI或领域特定的语言&#xff…

HTB-Obscurity

HTB-Obscurity信息收集8080端口立足www-data -> robertrobert -> rootsudo 注入hash捕获信息收集 8080端口 ”如果攻击者不知道你在使用什么软件&#xff0c;你就不会被黑客攻击!“&#xff0c;目标对web的指纹做了某些处理。 “‘SuperSecureServer.py’ in the secre…

【从零开始学Skynet】基础篇(六):MySql数据库安装操作

游戏服务端的另一项重要功能是保存玩家数据&#xff0c;Skynet提供了操作MySQL数据库、MongoDB数据库的模块。1、数据库安装 首先安装Mysql服务器&#xff0c;打开终端输入如下指令&#xff1a; sudo apt-get install mysql-server 按下回车&#xff0c;输入密码后开始安装&a…