HashMap是如何工作的

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

1 HashMap在JAVA中的怎么工作的?

基于Hash的原理

2 什么是哈希?

最简单形式的 hash,是一种在对任何变量/对象的属性应用任何公式/算法后, 为其分配唯一代码的方法。

一个真正的hash方法必须遵循下面的原则

哈希函数每次在相同或相等的对象上应用哈希函数时, 应每次返回相同的哈希码。换句话说, 两个相等的对象必须一致地生成相同的哈希码。

Java 中所有的对象都有 Hash 方法

Java中的所有对象都继承 Object 类中定义的 hashCode() 函数的默认实现。 此函数通常通过将对象的内部地址转换为整数来生成哈希码,从而为所有不同的对象生成不同的哈希码。

3 HashMap 中的 Node 类

Map的定义是: 将键映射到值的对象。

因此,HashMap 中必须有一些机制来存储这个键值对。 答案是肯定的。 HHashMap 有一个内部类 Node,如下所示:

    static class Node<K,V> implements Map.Entry<K,V> {final int hash;// 记录hash值, 以便重hash时不需要再重新计算final K key; V value;Node<K,V> next;...// 其余的代码}

当然,Node 类具有存储为属性的键和值的映射。 key 已被标记为 final,另外还有两个字段:next 和 hash。

在下面中, 我们将会理解这些属性的必须性。

4 键值对在 HashMap 中是如何存储的

键值对在 HashMap 中是以 Node 内部类的数组存放的, 如下所示:

transient Node<K,V>[] table;

哈希码计算出来之后, 会转换成该数组的下标, 在该下标中存储对应哈希码的键值对, 在此先不详细讲解hash碰撞的情况。

该数组的长度始终是 2 的次幂, 通过以下的函数实现该过程

static final int tableSizeFor(int cap) {int n = cap - 1;// 如果不做该操作, 则如传入的 cap 是 2 的整数幂, 则返回值是预想的 2 倍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;
}

其原理是将传入参数 (cap) 的低二进制全部变为 1, 最后加 1 即可获得对应的大于 cap 的 2 的次幂作为数组长度。

为什么要使用 2 的次幂作为数组的容量呢?

在此有涉及到 HashMap 的 hash 函数及数组下标的计算, 键(key)所计算出来的哈希码有可能是大于数组的容量的, 那怎么办? 可以通过简单的求余运算来获得, 但此方法效率太低。HashMap 中通过以下的方法保证 hash 的值计算后都小于数组的容量。

(n - 1) & hash

这也正好解释了为什么需要 2 的次幂作为数组的容量。 由于 n 是 2 的次幂, 因此, n - 1 类似于一个低位掩码。 通过与操作, 高位的hash值全部归零,保证低位才有效, 从而保证获得的值都小于 n。 同时, 在下一次 resize() 操作时, 重新计算每个 Node 的数组下标将会因此变得很简单, 具体的后文讲解。 以默认的初始值 16 为例

    01010011 00100101 01010100 00100101
&   00000000 00000000 00000000 00001111
----------------------------------00000000 00000000 00000000 00000101    //高位全部归零,只保留末四位// 保证了计算出的值小于数组的长度 n

但是, 使用了该功能之后, 由于只取了低位, 因此 hash 碰撞会也会相应的变得很严重。 这时候就需要使用 「扰动函数」

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

该函数通过将哈希码的高 16 位的右移后与原哈希码进行异或而得到, 以以上的例子为例

异或

此方法保证了高16位不变, 低16位根据异或后的结果改变。计算后的数组下标将会从原先的 5 变为 0。

使用了 「扰动函数」 之后, hash 碰撞的概率将会下降。 有人专门做过类似的测试, 虽然使用该 「扰动函数」 并没有获得最大概率的避免 hash 碰撞, 但考虑其计算性能和碰撞的概率, JDK 中使用了该方法, 且只 hash 一次。

5 哈希碰撞及其处理

在理想的情况下, 哈希函数将每一个 key 都映射到一个唯一的 bucket, 然而, 这是不可能的。 哪怕是设计在良好的哈希函数, 也会产生哈希冲突。

前人研究了很多哈希冲突的解决方法, 在维基百科中, 总结出了四大类

哈希碰撞解决方法

在 Java 的 HashMap 中, 采用了第一种 Separate chaining 方法(大多数翻译为拉链法)+链表和红黑树来解决冲突。

JDK8中HashMap结果

在 HashMap 中, 哈希碰撞之后会通过 Node 类内部的成员变量 Node<K,V> next; 来形成一个链表(节点小于8)或红黑树(节点大于8, 在小于6时会从新转换为链表), 从而达到解决冲突的目的。

static final int TREEIFY_THRESHOLD = 8;static final int UNTREEIFY_THRESHOLD = 6;

6 HashMap 的初始化

 public HashMap();public HashMap(int initialCapacity);public HashMap(Map<? extends K, ? extends V> m);public HashMap(int initialCapacity, float loadFactor); 

HashMap 中有四个构造函数, 大多是初始化容量和负载因子的操作。以 public HashMap(int initialCapacity, float loadFactor) 为例

public HashMap(int initialCapacity, float loadFactor) {// 初始化的容量不能小于0if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);// 初始化容量不大于最大容量if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// 负载因子不能小于 0if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);
}

通过该函数进行了容量和负载因子的初始化,如果是调用的其他的构造函数, 则相应的负载因子和容量会使用默认值(默认负载因子=0.75, 默认容量=16)。在此时, 还没有进行存储容器 table 的初始化, 该初始化要延迟到第一次使用时进行。

7 HashMap 中哈希表的初始化或动态扩容

所谓的哈希表, 指的就是下面这个类型为内部类Node的 table 变量。

transient Node<K,V>[] table;

作为数组, 其在初始化时就需要指定长度。在实际使用过程中, 我们存储的数量可能会大于该长度,因此 HashMap 中定义了一个阈值参数(threshold), 在存储的容量达到指定的阈值时, 需要进行扩容。

我个人认为初始化也是动态扩容的一种, 只不过其扩容是容量从 0 扩展到构造函数中的数值(默认16)。 而且不需要进行元素的重hash.

7.1 扩容发生的条件

初始化的话只要数值为空或者数组长度为 0 就会进行。 而扩容是在元素的数量大于阈值(threshold)时就会触发。

threshold = loadFactor * capacity

比如 HashMap 中默认的 loadFactor=0.75, capacity=16, 则

threshold = loadFactor * capacity = 0.75 * 16 = 12

那么在元素数量大于 12 时, 就会进行扩容。 扩容后的 capacity 和 threshold 也会随之而改变。

负载因子影响触发的阈值, 因此, 它的值较小的时候, HashMap 中的 hash 碰撞就很少, 此时存取的性能都很高, 对应的缺点是需要较多的内存; 而它的值较大时, HashMap 中的 hash 碰撞就很多, 此时存取的性能相对较低, 对应优点是需要较少的内存; 不建议更改该默认值, 如果要更改, 建议进行相应的测试之后确定。

7.2 再谈容量为2的整数次幂和数组索引计算

前面说过了数组的容量为 2 的整次幂, 同时, 数组的下标通过下面的代码进行计算

index = (table.length - 1) & hash

该方法除了可以很快的计算出数组的索引之外, 在扩容之后, 进行重 hash 时也会很巧妙的就可以算出新的 hash 值。 由于数组扩容之后, 容量是现在的 2 倍, 扩容之后 n-1 的有效位会比原来多一位, 而多的这一位与原容量二进制在同一个位置。 示例

扩容前后

这样就可以很快的计算出新的索引啦

7.3 步骤

  1. 先判断是初始化还是扩容, 两者在计算 newCap和newThr 时会不一样
  2. 计算扩容后的容量,临界值。
  3. 将hashMap的临界值修改为扩容后的临界值
  4. 根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
  5. 将旧数组的元素复制到table中。在该过程中, 涉及到几种情况, 需要分开进行处理(只存有一个元素, 一般链表, 红黑树)

具体的看代码吧

final Node<K, V>[] resize() {//新建oldTab数组保存扩容前的数组tableNode<K, V>[] oldTab = table;//获取原来数组的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//原来数组扩容的临界值int oldThr = threshold;int newCap, newThr = 0;//如果扩容前的容量 > 0if (oldCap > 0) {//如果原来的数组长度大于最大值(2^30)if (oldCap >= MAXIMUM_CAPACITY) {//扩容临界值提高到正无穷threshold = Integer.MAX_VALUE;//无法进行扩容,返回原来的数组return oldTab;//如果现在容量的两倍小于MAXIMUM_CAPACITY且现在的容量大于DEFAULT_INITIAL_CAPACITY} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)//临界值变为原来的2倍newThr = oldThr << 1;} else if (oldThr > 0) //如果旧容量 <= 0,而且旧临界值 > 0//数组的新容量设置为老数组扩容的临界值newCap = oldThr;else { //如果旧容量 <= 0,且旧临界值 <= 0,新容量扩充为默认初始化容量,新临界值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITYnewCap = DEFAULT_INITIAL_CAPACITY;//新数组初始容量设置为默认值newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//计算默认容量下的阈值}// 计算新的resize上限if (newThr == 0) {//在当上面的条件判断中,只有是初始化时(oldCap=0, oldThr > 0)时,newThr == 0//ft为临时临界值,下面会确定这个临界值是否合法,如果合法,那就是真正的临界值float ft = (float) newCap * loadFactor;//当新容量< MAXIMUM_CAPACITY且ft < (float)MAXIMUM_CAPACITY,新的临界值为ft,否则为Integer.MAX_VALUEnewThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?(int) ft : Integer.MAX_VALUE);}//将扩容后hashMap的临界值设置为newThrthreshold = newThr;//创建新的table,初始化容量为newCap@SuppressWarnings({"rawtypes", "unchecked"})Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];//修改hashMap的table为新建的newTabtable = newTab;//如果旧table不为空,将旧table中的元素复制到新的table中if (oldTab != null) {//遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中for (int j = 0; j < oldCap; ++j) {Node<K, V> e;//如果旧桶不为null,使用e记录旧桶if ((e = oldTab[j]) != null) {//将旧桶置为nulloldTab[j] = null;//如果旧桶中只有一个nodeif (e.next == null)//将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置newTab[e.hash & (newCap - 1)] = e;//如果旧桶中的结构为红黑树else if (e instanceof TreeNode)//将树中的node分离((TreeNode<K, V>) e).split(this, newTab, j, oldCap);else {  //如果旧桶中的结构为链表,链表重排,jdk1.8做的一系列优化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;elseloTail.next = e;loTail = e;} else {// 原索引+oldCapif (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 原索引放到bucket里if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 原索引+oldCap放到bucket里if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}

7.4 注意事项

虽然 HashMap 设计的非常优秀, 但是应该尽可能少的避免 resize(), 该过程会很耗费时间。

同时, 由于 hashmap 不能自动的缩小容量。 因此, 如果你的 hashmap 容量很大, 但执行了很多 remove 操作时, 容量并不会减少。 如果你觉得需要减少容量, 请重新创建一个 hashmap。

8 HashMap.put() 函数内部是如何工作的?

在使用多次 HashMap 之后, 大体也能说出其添加元素的原理:计算每一个key的哈希值, 通过一定的计算之后算出其在哈希表中的位置,将键值对放入该位置,如果有哈希碰撞则进行哈希碰撞处理。

而其工作时的原理如下(图是我很早之前保存的, 忘了出处了)
putVal工作流程

源码如下:

    /* @param hash         指定参数key的哈希值* @param key          指定参数key* @param value        指定参数value* @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value* @param evict        如果为false,数组table在创建模式中* @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K, V>[] tab;Node<K, V> p;int n, i;//如果哈希表为空,调用resize()创建一个哈希表,并用变量n记录哈希表长度if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;/*** 如果指定参数hash在表中没有对应的桶,即为没有碰撞* Hash函数,(n - 1) & hash 计算key将被放置的槽位* (n - 1) & hash 本质上是hash % n,位运算更快*/if ((p = tab[i = (n - 1) & hash]) == null)//直接将键值对插入到map中即可tab[i] = newNode(hash, key, value, null);else {// 桶中已经存在元素Node<K, V> e;K k;// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 将第一个元素赋值给e,用e来记录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;}// 链表节点的<key, value>与put操作<key, value>相同时,不做重复操作,跳出循环if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 找到或新建一个key和hashCode与插入元素相等的键值对,进行put操作if (e != null) { // existing mapping for key// 记录e的valueV oldValue = e.value;/*** onlyIfAbsent为false或旧值为null时,允许替换旧值* 否则无需替换*/if (!onlyIfAbsent || oldValue == null)e.value = value;// 访问后回调afterNodeAccess(e);// 返回旧值return oldValue;}}// 更新结构化修改信息++modCount;// 键值对数目超过阈值时,进行rehashif (++size > threshold)resize();// 插入后回调afterNodeInsertion(evict);return null;}

在此过程中, 会涉及到哈希碰撞的解决。

9 HashMap.get() 方法内部是如何工作的?

    /*** 返回指定的key映射的value,如果value为null,则返回null* get可以分为三个步骤:* 1.通过hash(Object key)方法计算key的哈希值hash。* 2.通过getNode( int hash, Object key)方法获取node。* 3.如果node为null,返回null,否则返回node.value。** @see #put(Object, Object)*/public V get(Object key) {Node<K, V> e;//根据key及其hash值查询node节点,如果存在,则返回该节点的value值return (e = getNode(hash(key), key)) == null ? null : e.value;
}

其最终是调用了 getNode 函数。 其逻辑如下

getNode 工作逻辑

源码如下:

    /*** @param hash 指定参数key的哈希值* @param key  指定参数key* @return 返回node,如果没有则返回null*/final Node<K, V> getNode(int hash, Object key) {Node<K, V>[] tab;Node<K, V> first, e;int n;K k;//如果哈希表不为空,而且key对应的桶上不为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//如果桶中的第一个节点就和指定参数hash和key匹配上了if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))//返回桶中的第一个节点return first;//如果桶中的第一个节点没有匹配上,而且有后续节点if ((e = first.next) != null) {//如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点if (first instanceof TreeNode)return ((TreeNode<K, V>) first).getTreeNode(hash, key);//如果当前的桶不采用红黑树,即桶中节点结构为链式结构do {//遍历链表,直到key匹配if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}//如果哈希表为空,或者没有找到节点,返回nullreturn null;
}

作者:阿进的写字台

出处:https://www.cnblogs.com/homejim/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

转载于:https://my.oschina.net/watsonos/blog/2962123

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

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

相关文章

如何做到十五分钟领略PowerBI的DAX精华及框架

有小伙伴问&#xff0c;如何最快速理解整套 DAX 精华体系&#xff0c;例如&#xff1a;15分钟。这虽然是个不可能完成的任务&#xff0c;但在借助脑图PPT的强大能力下&#xff0c;还是可以做到的。如果你从没接触过 DAX&#xff0c;但未来要学习 DAX&#xff0c;以下视频值得看…

springboot配置允许跨域访问

2019独角兽企业重金招聘Python工程师标准>>> 因springboot框架通常用于前后端分离项目&#xff0c;因此需配置后台允许跨域访问&#xff08;具体看注释&#xff09;&#xff0c;配置类如下&#xff0c;将该类加入工程中即可。 import org.springframework.context.a…

机器视觉传感器选型,交互作用决定取舍

目前&#xff0c;如何选择机器视觉传感器在当代的应用可谓是越来越广泛&#xff0c;如何选择机器视觉传感器是值得我们好好学习的&#xff0c;现在我们就深入了解如何选择机器视觉传感器。 相机是机器视觉系统的眼睛&#xff0c;而相机的心脏是图像传感器。传感器的选择取决于准…

使用qrcode类制作二维码

<?phprequire_once ./phpqrcode/phpqrcode.php;/** 地址:http://phpqrcode.sourceforge.net/ 下载qrcode类* param string $url 要生成的链接* param bool $local 是否生成本地文件 * param string $logo 中间图片地址 */ echo qrcode(https://www.cnblogs.co…

《Hadoop大明白》【1】Hadoop的核心组件

为什么80%的码农都做不了架构师&#xff1f;>>> 本书适用于想对大数据平台Hadoop有深入了解的程序员朋友&#xff0c;会帮助你以后在应用开发方面趟平一些坑。 1&#xff0c;Hadoop 是一个系列大数据组件构成的应用平台&#xff0c;可由多个廉价机器搭建集群&#…

.NET7是如何优化Guid.Equals性能的?

简介在之前的文章中&#xff0c;我们多次提到 Vector - SIMD 技术&#xff0c;也答应大家在后面分享更多.NET7 中优化的例子&#xff0c;今天就带来一个使用 SIMD 优化Guid.Equals()方法性能的例子。为什么 Guid 能使用 SIMD 优化&#xff1f;首先就需要介绍一些背景知识&#…

缺氧游戏黑科技计算机,《缺氧》游戏内参数修改图文详解

很多玩家都很喜欢缺氧这款游戏&#xff0c;有时候因为一些不可告人的秘密我们需要修改游戏中的内容来达到简化我们的生存难度&#xff0c;这样就需要修改游戏的脚本&#xff0c;所幸《缺氧 》对于这个问题很宽容&#xff0c;完全没有加密地图的生成脚本&#xff0c;让我们可以完…

Python 项目实践三(Web应用程序)第四篇

接着上节继续学习&#xff0c;本章将建立用户账户 Web应用程序的核心是让任何用户都能够注册账户并能够使用它&#xff0c;不管用户身处何方。在本章中&#xff0c;你将创建一些表单&#xff0c;让用户能够添加主题和条目&#xff0c;以及编辑既有的条目。你还将学习Django如何…

新手想买二手车 先看看买车后这五个步骤吧

买二手车你该知道 很多人因为资金短缺又或者是想要一辆便宜车“练手”而选择去买一辆价格低廉&#xff0c;有着一定车龄的二手车。很多人看中二手车正正是因为便宜&#xff0c;以为是购买以后基本不需要再投入新的花费&#xff0c;殊不知这是非常错误的想法&#xff0c;因为以下…

十六进制编辑器--ImHex

十六进制编辑器是用于编辑单个字节数据的软件应用程序&#xff0c;主要由程序员或系统管理员使用。常规文本编辑器和十六进制编辑器之间的区别在于常规编辑器表示文件的逻辑内容&#xff0c;而十六进制编辑器表示文件的物理内容。十六进制编辑器可以让你以十六进制的形式查看或…

奥迪坚SVRM(Screen-Voice Recording Manager)录屏软件正式发布

奥迪坚SVRM(Screen-Voice Recording Manager)能够对座席通话同步录音的同时进行座席操作录屏 实时监控座席屏幕操作&#xff0c;及时纠正操作问题。 座席质检可以边听边看&#xff0c;为KPI考核提供依据。 利用优秀座席操作记录对座席进行培训。 监控坐席人员对敏感信息访问次数…

小米:开源不仅要站在巨人的肩膀上,还要为巨人指方向

今天上午&#xff0c;第一届小米开源技术峰会在北京举行&#xff0c;会上&#xff0c;小米人工智能与云平台副总裁崔宝秋致开场词&#xff0c;并发表了《小米开源之路》的演讲。 崔宝秋强调小米一直在推动开源&#xff0c;也是开源的倡导者。他告诉我们雷军创立小米的其中一个重…

《设计模式》3.结构型模式

点击进入我的博客 3.1 适配器模式 适配器模式把一个类的接口变换成客户端所期待的另一种接口&#xff0c;使得原本因接口不匹配而无法在一起工作的两个类能够在一起工作。 3.1.1 类的适配器结构 目标&#xff08;Target&#xff09;角色&#xff1a;这就是所期待得到的接口&…

最快的计算机操作,世界十大最快的超级计算机

最近&#xff0c;《联邦储备技术》杂志对全球超级计算机进行了排名&#xff0c;并从中选出了十个最快的超级计算机. 其中&#xff0c;中国有两台超级计算机进入了榜单&#xff0c;而“天河2号”则依靠双精度浮点算术峰. 速度达到了每秒5490亿次&#xff0c;占据了王位.这也是两…

苹果iOS 10.3.1修复博通Wi-Fi芯片重大安全漏洞

如果你还没有将设备升级到 iOS 10.3.1 的话&#xff0c;那么现在是个机会了。因为不久前发布的 iOS 10.3.1&#xff0c;修复了 iPhone 中博通 Wi-Fi 芯片的一个重大安全漏洞&#xff0c;该安全漏洞可能会使在 Wi-Fi 范围内的攻击者在智能手机上注入并运行代码。 Google Project…

台积电放大招:甩开英特尔 7nm和5nm芯片将诞生

北京时间1月20日消息&#xff0c;据科技网站AppleInsider报道&#xff0c;近几年来台积电的发展势头相当猛&#xff0c;该公司总裁兼联合CEO刘德音(Mark Liu)在最近的投资者会议中表示&#xff0c;预计今年年末公司就将正式量产10nm晶圆。此外&#xff0c;台积电7nm研发一如预期…

01: 实现注册登录功能

目录&#xff1a;抽屉项目之js最佳实践 01: 实现注册登录功能 02: 实现发布帖子功能 03: 将帖子展示到页面、点赞 04: 层级评论 目录&#xff1a; 1.1 显示、隐藏 "登录/注册" 菜单1.2 注册功能1.3 登录功能1.4 获取当前用户数量1.1 显示、隐藏 "登录/注册"…

计算机系统怎么算页面大小,电脑网页的设计尺寸是多少

电脑网页的设计尺寸是多少刚入门的网页设计师可能对电脑网页的设计尺寸大小存在疑问&#xff0c;以下百分网小编整理的电脑网页的设计尺寸&#xff0c;希欢迎阅读!  对大于30W台客户端用户进行测试&#xff0c;得到的测试数据如下(数据来源于网络)&#xff1a;安全分辨率为10…

WPF 托盘闪烁

WPF 托盘闪烁控件名&#xff1a;NotifyIcon作者&#xff1a;WPFDevelopersOrg - 弈虎、驚鏵原文链接&#xff1a; https://github.com/WPFDevelopersOrg/WPFDevelopers框架使用大于等于.NET40。Visual Studio 2022。项目使用 MIT 开源许可协议。接着上一篇基础托盘。新增如下…

Information Retrieval 倒排索引 学习笔记

一&#xff0c;问题描述 在Shakespeare文集&#xff08;有很多文档Document&#xff09;中&#xff0c;寻找哪个文档包含了单词“Brutus”和"Caesar"&#xff0c;且不包含"Calpurnia"。这其实是一个查询操作&#xff08;Boolean Queries&#xff09;。 在U…