HashMap 源码学习-jdk1.8

1、一些常量的定义

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

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

    // 默认的初始化长度 16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 默认的最大容量 2^30static 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 = 容量 * loadFactorfinal float loadFactor;

2、构造器

HashMap提供了三个构造器。

    // 无参构造器,使用默认加载因子 0.75public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}// 只传入初始化容量,也会使用默认加载因子 0.75public 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 keyV 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 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);}// hashmap 初始化的时候,是将数组初始化长度赋值给了threshold,这里开始才是变成扩容阈值。threshold = newThr;// 创建新的数组,并将新数组赋值给tableHashMap.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();} // 先拿到当前坐标下的头节点 ,赋值给 eelse 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 和 hashK 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相等的时候,继续一系列的判断,最终得到direlse 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;// 找要删除的元素,赋值给nodeif (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;
}

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

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

相关文章

网安播报 | AI生成代码对组织和软件供应链构成了重大风险

1、AI生成代码对组织和软件供应链构成了重大风险 根据Veracode最新发布的软件安全报告&#xff0c;42%的应用程序和71%的组织中普遍存在软件安全债务&#xff0c;而AI生成代码的激增将导致安全债务问题恶化并对软件供应链构成重大风险。更令人担忧的是&#xff0c;46%的组织持续…

短视频新媒体的福音:视频抽插帧AI效率是人工的100倍以上

进入全民短视频时代&#xff0c;人像视频的拍摄也正在迈向专业化。随着固化审美的瓦解&#xff0c;十级磨皮的网红滤镜被打破&#xff0c;多元化的高级质感成为新的风向标&#xff0c;“美”到每一帧是人们对动态视频提出的更高要求。 目前&#xff0c;大部分手机均可记录主流的…

ABB触摸屏维修工控机显示屏维修CP405 A1/A0

ABB人机界面维修常见故障&#xff1a;黑屏白屏花屏&#xff0c;按触摸屏无反应或反应慢触摸不好&#xff0c;内容错乱&#xff0c;进不了系统界面&#xff0c;无背光背光暗&#xff0c;有背光无字符&#xff0c;不能通信&#xff0c;按键无无反应等均可维修。 此维修检测方法有…

信号信号槽

三、信号槽 概念 信号和槽是两种函数&#xff0c;这是Qt在C基础上新增的特性&#xff0c;类似于其他技术中的回调的概念。 信号槽通过程序员提前设定的“约定”&#xff0c;可以实现对象之间的通信&#xff0c;有两个先决条件。 通信的对象都是在QOBject类中派生出来的。 QOBje…

springboot212球队训练信息管理系统

球队训练信息管理系统设计与实现 摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装球队训练信息管理系统软件…

代理IP为什么会有延迟?

在当今信息高速发展的时代&#xff0c;随着代理IP在数据采集、网络安全和匿名浏览等领域的应用&#xff0c;已成为网络技术中不可或缺的一环。然而&#xff0c;用户在使用代理IP时经常会遇到一个问题——延迟。 那我们要如何解决这个问题呢&#xff1f; 这需要从代理IP的原理说…

第七章 正则表达式

目录 1.1. 概念&#xff1a; 1.2. 基本正则表达式 1.2.1. 常见元字符 1.2.2. POSIX字符类 1.2.3. 示例 1.3. 扩展正则表达式 1.3.1. 概念 1.3.2. 示例 1.1. 概念&#xff1a; 在进行程序设计的过程中&#xff0c;用户会不可避免地遇到处理某些文本的情况。有的时候&am…

android 15

https://android-developers.googleblog.com/2024/02/first-developer-preview-android15.html android 15的预览版出了&#xff0c;这个版本的发布计划大概是这样的&#xff08;大约是今年8月发布最终版本&#xff09; https://developer.android.com/about/versions/15/over…

Day34 线程Thread

文章目录 1.什么是线程1.1 概念1.2 进程和线程的区别1.3 线程资源 2.函数接口2.1 创建线程 : pthread_create2.2 退出线程: pthread_exit2.3 回收线程资源 1.什么是线程 1.1 概念 线程是一个轻量级的进程&#xff0c;为了提高系统的性能引入线程。 在同一个进程中可以创建的多…

【YOLO v5 小目标改进】SPD-Conv

SPD-Conv 提出背景SPD-Conv YOLO v5 小目标改进定义 SPD-Conv导入SpaceToDepth模块修改 .yaml 文件 提出背景 论文&#xff1a;https://arxiv.org/pdf/2208.03641v1.pdf 代码&#xff1a;https://github.com/labsaint/spd-conv 文章提出一个新的卷积神经网络(CNN)构建块&…

echarts多y轴样式重叠问题

1、主要属性设置 yAxis: [{//y轴1nameTextStyle: {align: "right",padding: 0}},{//y轴2nameTextStyle: {align: "left",padding: 0}},{//y轴3axisLabel: {margin: 50},nameTextStyle: {align: "left",padding: [0, 0, 0, 50]},axisPointer: {l…

二轮充电桩协议商用通讯协议

文章目录 一、基础通信协议框架二、命令的定义各数据单位定义&#xff1a; 三、设备上传3.0.1、设备心跳包&#xff08;01 指令&#xff09;3.0.2、设备注册包&#xff08;20 指令&#xff09;3.0.3、设备获取服务器时间&#xff08;22 指令&#xff09;3.1、设备心跳包&#x…

基于springboot+vue的靓车汽车销售网站(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

ChatGPT在数据处理中的应用

ChatGPT在数据处理中的应用 今天的这篇文章&#xff0c;让我不断体会AI的强大&#xff0c;愿人类社会在AI的助力下走向更加灿烂辉煌的明天。 扫描下面二维码注册 ​ 数据处理是贯穿整个数据分析过程的关键步骤&#xff0c;主要是对数据进行各种操作&#xff0c;以达到最终的…

【Git】:标签功能

标签功能 一.标签操作二.推送远程标签 标签 tag &#xff0c;可以简单的理解为是对某次commit的⼀个标识&#xff0c;相当于起了⼀个别名。例如&#xff0c;在项⽬发布某个版本的时候&#xff0c;针对最后⼀次commit起⼀个v1.0这样的标签来标识⾥程碑的意义。这有什么⽤呢&…

uniapp微信小程序解决上方刘海屏遮挡

问题 在有刘海屏的手机上&#xff0c;我们的文字和按钮等可能会被遮挡 应该避免这种情况 解决 const SYSTEM_INFO uni.getSystemInfoSync();export const getStatusBarHeight ()> SYSTEM_INFO.statusBarHeight || 15;export const getTitleBarHeight ()>{if(uni.get…

Ubuntu系统本地部署Inis博客结合内网穿透实现远程访问本地站点

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总…

统计图雷达图绘制方法

统计图雷达图绘制方法 常用的统计图有条形图、柱形图、折线图、曲线图、饼图、环形图、扇形图。 前几类图比较容易绘制&#xff0c;饼图环形图绘制较难。 还有一种雷达图的绘制也较难&#xff0c;今提供雷达图的绘制方法供参考。 本方法采用C语言的最基本功能&#xff1a; &am…

GoLand 相关

goland 下载依赖 go mod tidy&#xff1a;保持依赖整洁 go mod tidy 命令的作用是清理未使用的依赖&#xff0c;并更新 go.mod 以及 go.sum 文件。 go mod tidy 和 go mod vendor 两个命令是维护项目依赖不可或缺的工具。go mod tidy 确保了项目的 go.mod 文件精简且准确&…

精通Django模板(模板语法、继承、融合与Jinja2语法的应用指南)

模板&#xff1a; 基础知识&#xff1a; ​ 在Django框架中&#xff0c;模板是可以帮助开发者快速⽣成呈现给⽤户⻚⾯的⼯具模板的设计⽅式实现了我们MVT中VT的解耦(M: Model, V:View, T:Template)&#xff0c;VT有着N:M的关系&#xff0c;⼀个V可以调⽤任意T&#xff0c;⼀个…