最新要闻
- 通讯!0糖0脂0卡 三种口味 哪吒气泡水1.66元/瓶史底
- 世界今亮点!逆天!男子称在国美买家电两年没到货 花费超两万
- 全球热消息:影驰首次涉足游戏显示器:27寸2K屏、165Hz高刷
- 奔驰史诗级调整:19款车将停产!不挣钱的不要
- 世界热门:宁德时代两大杀手级新品电池正式来了:新能源汽车市场要变天?
- 14.9mm纤薄机身惊艳:联想小新Air 14 2023烟霞紫亮相
- 观热点:1099元起 真我推出V30/V30t新机:天玑700入门U
- 环球热点评!3月1日上线全国外卖服务?抖音回应:无具体时间表
- 《GTA5》母公司T2 CEO:ChatGPT等AI将提高游戏行业的标准
- 业界功率最高!一加27W冰点散热背夹发布 219元
- 蔚来:自家充电桩仅24%时间给蔚来充电 比亚迪、特斯拉用爽了
- 世界微资讯!ChatGPT幕后创始人拿微软100亿 是在下一步大棋
- 【热闻】你出国耍吗?多条国际航班机票降至千元 泰国往返国内不到2000元
- 焦点观察:日产扛把子!新款轩逸/轩逸e-POWER官图发布:能救市吗?
- 全球动态:一加Ace 2全球首发1.5K灵犀触控屏:120Hz刷新率、1450nit高亮度
- 每日消息!知名玩家爆料魔兽国服已签新代理:暴雪相中上海一公司
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
世界报道:HashMap源码分析 (基于JDK1.8)
HashMap
本文讲解的HashMap以及源代码都是基于JDK1.8
背景引入
数组
优:读取修改快劣:增加删除慢
原因:数组可以根据下标直接定位到指定位置的数据进行读取和修改,但增加和删除需要开辟一个新数组并移动增加和删除后的数据到新数组并返回。
(资料图片仅供参考)
链表
优:增加删除快劣:读取修改慢
原因:链表增加和删除只需断开指定位置的两端节点,但读取的时候只能从头/尾开始往另一方向读取。
拓展知识点:
数组和链表迭代的方式不同 ArrayList实现了RandomAccess接口 这是一个标记接口,标注是否可以随机访问 ArrayList使用数组实现,可以随机访问 经过测试 使用for循环遍历ArrayList更快 而LinkedList没有实现这个RandomAccess接口 不支持随机访问,使用迭代器遍历更快 RandomAccess接口的作用。
正是数组和链表各有各的优势,所以引入了散列表,结合了两者的优势尽可能的降低劣势带来的影响。
简介
Hash
哈希:英文是Hash,也称为散列基本原理就是把任意长度输入,转化为固定长度输出 这个映射的规则就是Hash算法,而原始数据映射的二进制串就是Hash值
Hash特点:
从Hash值不可以反向推导出原始数据 (因为异或的缘故无法反推)
输入数据的微小变化会得到完全不同的Hash值相同的数据一定可以得到相同的值
哈希算法的执行效率要高效,长的文本也能快速计算Hash值
Hash算法的冲突概率要小
HashMap
HashMap ,是一种散列表,用于存储 key-value 键值对的数据结构,每一个键值对也叫做Entry,一般翻译为“哈希表”,提供平均时间复杂度为 O(1) 的、基于 key 级别的 get/put 等操作。
类图
双列集合定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap
下面针对各个实现类的特点做一些说明:
(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
存储结构
HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的
改变链表结构的默认条件:1. 单链表中的元素个数大于8时 2. 桶数组中的元素个数大于64时【二者满足其一即可】
table数组是一个Node [] table的数组,存放node节点(Node是HashMap的一个内部类)
static class Node implements Map.Entry { final int hash; //当前节点经过hash扰动函数和路由算法后的值(存放位置的下标) final K key; V value; Node next; //下一个node节点 Node(int hash, K key, V value, Node next) { ... } public final K getKey(){ ... } public final V getValue() { ... } public final String toString() { ... } public final int hashCode() { ... } public final V setValue(V newValue) { ... } public final boolean equals(Object o) { ... }}
Hash存储过程
比较简单的描述,详细的可以看put方法流程图
以存储该键值对为例
map.put("侦探","conan");
① 系统将调用”侦探”这个key的hashCode()方法得到其hashCode 值(该方法适用于每个Java对象),
② 然后再通过Hash算法的后两步运算(高位运算 (扰动函数) 和取模运算 (路由算法) )来定位该键值对的存储位置
Hash冲突
由来:不同的数据经过hash扰动函数hash( )、路由算法 ( hash(x) & (n-1) ) 后得到的值可能相同,此时就会存到同一个桶位,这就叫hash冲突。
解决hash冲突方法:
开放寻址法
链表法(JDK1.8使用链表+红黑树)
当不同的数据经过hash算法后到达同一个桶位,该桶内的数据就会链化。拉链法的链子就会很长,那么就会降低查找速度。
此时桶数组小了容易发生hash冲突,hash算法要是区分数据不明显也会导致hash冲突,所以仅仅时靠链表法来缓解hash冲突是不够的,也需要号的hash算法以及扩容机制来预防hash冲突。
Hash算法
hash算法包括hash扰动函数以及路由算法。Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
Hash扰动函数
作用:分散hashcode,使相似的数据有着截然不同的hash值
//hash扰动函数:加入了对高16位的特征,更见分散hash值//(h = key.hashCode()) ^ (h >>> 16):拿自己本身和本身右进制16位后的数组做异或运算,得到的数字就带有高16位的特征//疑问:为什么不直接使用整个hash而是要得到前16位为0的数字?(整合hash就包括本身的高低16位数的特征)//答案:因为hashcode的数据类型是int,有符号位所以int在正数范围内是16位。所以需要hash得到一个16的数字static final int hash(Object key) { int h;//key对于的hashcode值 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
代码中的h>>>16就是jdk1.8新引入的高位运算算法,作用:使hashcode的高位数字特征也能一并体现再hashcode中,更加分散hash值。通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
n为table长度
路由算法
jdk1.8没有封装成方法,而是直接使用以下公式
h & (table.length-1); //h是前面hash扰动得到的值
HashMap扩容
注意点:JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是JDK1.8不会倒置 (具体可以看下文的Resize方法讲解)
先了解几个源码变量:
//当前hash表中实际存在的元素个数transient int size;//当前hash表的结构修改次数(注意:改查不算结构修改,增删才算结构修改)transient int modCount;//扩容阈值:当hash表中的元素超过该值时,触发扩容【通过赋值因子计算得来:threshold=loadFactor * capacity】 capacity是桶数组长度int threshold;//负载因子【默认0.75】final float loadFactor;
Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。
threshold扩容阈值就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择。(一般情况不修改,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1)
在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数)(如何保证为2的n次方下面构造方法有讲),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,Hashtable初始化桶大小为11,就是桶大小设计为素数的应用(Hashtable扩容后不能保证还是素数)。
必须为2的n次方的HashMap非常规设计目的:为了在取模 (路由算法) 和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了 (扰动函数) 高位参与运算的过程。
路由算法优化:因为要提升性能 &比%快,所以只有当容量n为2的n次方时,(n - 1) & hash == hash % (n - 1) 才成立!
扩容优化:扩容的时候方便数据迁移。(具体如何方便后文有讲)
Hash源码
构造函数
小结:扩容是一个特别耗性能的操作,所以在创建hashmap的时候尽量估算capacity容量
先认识以下变量(和前面扩容认识的变量一样)
//桶数组transient Node[] table;//键值对对象transient Set> entrySet;//当前hash表中实际存在的元素个数transient int size;//当前hash表的结构修改次数(改查不算结构修改,增删才算结构修改)transient int modCount;//扩容阈值:当hash表中的元素超过该值时,触发扩容【通过赋值因子计算得来:threshold=loadFactor * capacity】 capacity是桶数组长度int threshold;//负载因子final float loadFactor;
四种构造方法
//指定容量和负载因子构造public HashMap(int initialCapacity, float loadFactor) { //1. 做校验: //1.1 容量合法 容量要在0~最大值内 //1.2 负载因子必须大于0 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); //2. 处理输入的capacity,保证最后的capacity为2的n次方 this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); //转为capacity为2的n次方}//指定容量构造public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}//无参构造public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}//传map参数构造public HashMap(Map extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false);}
保证capacity为2的n次方的算法:
//将capacity转化为大于capacity的最小的2的n次方//原理:进来16结果是16 进来15结果是16//0001 1101 1100 => 0001 1111 1111 => 0010 0000 0000//解释:// 1. 先将进来的数字-1(保证边缘不出错,如:边缘的16出去也是16)// 2. 如何通过或运算,将最高位以下的0都转成1// 3. 最后再+1,就可以得到capacity最小的2的n次方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;}
在以上构造方法中并没有看到 table
数组的初始化,只对静态变量capacity、loadFactor赋值了,因为它是延迟初始化。只有在插入第一条数据的时候才会初始化(后面put方法中有体现)
PUT方法
put过程图
putVal代码逻辑图:
put代码:put调用putVal
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab:引用当前hashMap的散列表 //p:表示当前散列表的元素 //n:表示散列表数组的长度 //i:表示路由寻址结果 Node[] tab; Node p; int n, i; //1.前面创建时延迟初始化,节约内存。在第一次插入数据调用putVal的时候才初始化hashmap中最消耗内存的散列表 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //2.该桶位为空时直接插入:通过路由算法(tab.length-1)& hash 得到桶位并插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //3.该桶位有元素时(链表/红黑树): else { //e:不为null的话,找到了一个与当前要插入的A元素的key一致的B元素(B元素是桶内的) //k:表示临时的一个key Node e; K k; //3.1 要插入的元素的key与桶内元素的key一致,则进行替换修改操作 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //3.2 遇到该桶位已经是红黑树的情况 else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); //3.3 遇到该桶位为链表的情况 else { //3.3.1循环遍历链表,比对key(要插入的key和桶内链表的all key)是修改数据还是插到尾部 for (int binCount = 0; ; ++binCount) { //① 到了末尾都没找到相同的key就插入到尾部 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //② 添加数据后,链长度是否触发数化操作。 //binCount >= TREEIFY_THRESHOLD - 1:当binCount=7时就是第九个元素,因为从0开始并且该桶已经有元素在了 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //③ 遇到相同的key就替换修改数据 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //3.4 替换数据;当e!=null说明有需要替换的数据,就替换数据 if (e != null) { // existing mapping for key V oldValue = e.value; //参数允许替换 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //4. 如果是增加操作,就增加修改结构的次数(如果是修改操作前面会return的) ++modCount; //5. 插入新元素长度加一,判断是否触发扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}
Resize方法
扩容方法resize ( )
代码:
final Node[] resize() { //oldTab:引用扩容前的哈希表 Node[] oldTab = table; //oldCap:表示扩容之前table数组的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldThr:表示扩容之前的扩容阈值,触发本次扩容的阈值 int oldThr = threshold; //newCap:扩容之后table数组的大小 //newThr:扩容之后,下次再次触发扩容的条件 int newCap, newThr = 0; //条件成立,说明hashMap中的散列表已经初始化过了,是一次正常的扩容 if (oldCap > 0) { //如果当前容量已经大于最大容量则不能扩容,并且修改扩容条件为int最大值,防止下次触发扩容条件 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //正常扩容*2;条件是旧的容量大于16,并且扩容后的容量(*2后的)小于最大容量 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // oldCap == 0 && oldThr > 0 说明散列函数未初始化,但创建的时候有指定容量 // 1.new HashMap(initCap,loadFactor) // 2.new HashMap(intiCap) // 3.new HashMap(map) 并且Map有数据 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //oldCap == 0,散列表未初始化过,且创建的时候没指定容量。此时就需要设置默认值 // 1.new HashMap(); else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newThr为0的时候,通过newCap和loadFactor计算出一个newThr if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 更新阈值为计算出来的 newThr threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //创建一个很大的数组(如果是默认初始化就是16,是扩容就是*2,是指定初始化就是大于capacity最小的2的n次方) Node[] newTab = (Node[])new Node[newCap]; //更新table的引用 table = newTab; //原数组中有值,需要做数据迁移 if (oldTab != null) { //遍历原数组中的元素 for (int j = 0; j < oldCap; ++j) { //e:暂存当前的node节点 Node e; //当前桶位有节点:单个数据、链表数据、红黑树数据 if ((e = oldTab[j]) != null) { //前面e已经记录了当前节点,所以当前节点置为null便于 GC回收 oldTab[j] = null; //当前桶位是单个数据,就对该数据重新路由算法到新数组 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //当前桶位是红黑树 else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); //当前桶位是链表,也要对全部数据重新路由到新数组 else { // preserve order // 低位链表:存放在扩容之后的数组的下标的位置,与当前数组下标位置一致 Node loHead = null, loTail = null; // 高位链表:存放在扩容之后数组的下标位置为:当前数组下标位置 + 扩容之前数组的长度 Node hiHead = null, hiTail = null; //低高位链表例子:x可能为0 1 //x1111在原数组的15,根据路由算法到新数组中要么在15要么在31,因为x只有0 1变化 //存放当前链表的一个元素 Node 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;}
扩容时数据迁移算法:
我们使用的是2次幂的扩展(指长度扩为原来2倍),所以元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置(看下图可以明白这句话的意思)n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。下图以16扩容为32的resize示意图:
GET方法
代码:
//根据传入的key,得到其hash然后去查是否有该值public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node getNode(int hash, Object key) { // tab:引用当前hashMap的散列表 // first:桶位中的头元素 // e:临时node元素 // n:table数组元素 Node[] tab; Node first, e; int n; K k; //有数据可查的时候:桶hash存在并且hash路由算法得到的桶不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //桶位的头节点就是要找的 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //桶位里不止一个数据 if ((e = first.next) != null) { //桶位里是红黑树,查找红黑树O(LogN) if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); //桶位里是链表,就遍历查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}
Remove方法
public V remove(Object key) { Node e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;}//逻辑:先查到然后标记,再删除标记的元素final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { // tab:引用当前hashMap的散列表 // p:当前node元素 // n:表示散列表数组长度 // index:表示寻址结果 Node[] tab; Node p; int n, index; //查找: //有数据时:判断hash桶是否为空,并且根据hash路由到的桶位是否存在 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { // node:查找到的结果 // e:当前Node的下一个元素 Node node = null, e; K k; V v; //桶位节点就是要找到节点,直接标记 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //桶位节点数据有多个,可能是红黑树or链表 else if ((e = p.next) != null) { //红黑树查找 if (p instanceof TreeNode) node = ((TreeNode)p).getTreeNode(hash, key); //链表循环查找 else { 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)))) { //删红黑树节点 if (node instanceof TreeNode) ((TreeNode)node).removeTreeNode(this, tab, movable); //删桶位节点 else if (node == p) tab[index] = node.next; //删链表节点 else p.next = node.next; //删除操作修改了结构所以++ ++modCount; //删除操作使数量-- --size; afterNodeRemoval(node); return node; } } return null;}
Replace方法
//根据 k 和 v 替换public V replace(K key, V value) { Node e; if ((e = getNode(hash(key), key)) != null) { V oldValue = e.value; e.value = value; afterNodeAccess(e); return oldValue; } return null;}//根据 k oldValue newValue 替换 public boolean replace(K key, V oldValue, V newValue) { Node e; V v; if ((e = getNode(hash(key), key)) != null && ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) { e.value = newValue; afterNodeAccess(e); return true; } return false;}
世界报道:HashMap源码分析 (基于JDK1.8)
cmd的一些命令
焦点速看:Python教程:高级特性
天天快看点丨vue3 | slots
通讯!0糖0脂0卡 三种口味 哪吒气泡水1.66元/瓶史底
世界今亮点!逆天!男子称在国美买家电两年没到货 花费超两万
全球热消息:影驰首次涉足游戏显示器:27寸2K屏、165Hz高刷
奔驰史诗级调整:19款车将停产!不挣钱的不要
世界热门:宁德时代两大杀手级新品电池正式来了:新能源汽车市场要变天?
VGA、TFT显示模块——verilog实现
天天看热讯:智能运维|AIRIOT智慧光伏管理解决方案
定时任务
14.9mm纤薄机身惊艳:联想小新Air 14 2023烟霞紫亮相
观热点:1099元起 真我推出V30/V30t新机:天玑700入门U
环球热点评!3月1日上线全国外卖服务?抖音回应:无具体时间表
《GTA5》母公司T2 CEO:ChatGPT等AI将提高游戏行业的标准
记录--实时音视频功能简析(live-pusher与live-player)
当前资讯!全景剖析阿里云容器网络数据链路(三):Terway ENIIP
全球看点:Amazon RDS 教程_编程入门自学教程_菜鸟教程-免费教程分享
整合MQTT
当前简讯:京东一面:MySQL 中的 distinct 和 group by 哪个效率更高?问倒一大遍。。
业界功率最高!一加27W冰点散热背夹发布 219元
蔚来:自家充电桩仅24%时间给蔚来充电 比亚迪、特斯拉用爽了
世界微资讯!ChatGPT幕后创始人拿微软100亿 是在下一步大棋
【热闻】你出国耍吗?多条国际航班机票降至千元 泰国往返国内不到2000元
焦点观察:日产扛把子!新款轩逸/轩逸e-POWER官图发布:能救市吗?
3.SpringSecurity+登录功能+jwt校验过滤器+redis配置
当前消息!K8S的基础概念
今亮点!elementui中el-table表头动态筛选条件未及时更新问题解决
世界新消息丨C#高级编程2
RocketMQ Streams拓扑构建与数据处理过程
全球动态:一加Ace 2全球首发1.5K灵犀触控屏:120Hz刷新率、1450nit高亮度
每日消息!知名玩家爆料魔兽国服已签新代理:暴雪相中上海一公司
全球热点!土耳其再发生5.2级地震:中国卫星传回土耳其地震震中图像
全麦+黑麦:舌里0脂肪全麦面包14.9元2斤40片大促
国产版ChatGPT!百度版已成功注册ERNIE商标:最快3月开放
强大的word插件:不坑盒子下载安装使用图文讲解
ChatGPT横空出世,虽然会改BUG,但程序员也不用慌
天天简讯:强大的word插件,让工作更高效:不坑盒子 2023版
pandas 用户数据分析2
车顶维权女车主:重启战斗模式 希望今年能明确特斯拉案件结果
小米首款二合一笔记本!小米笔记本12.4图赏
每日速看!陪伴十年被撞报废!男子躲过一劫磕头跪别爱车 网友:它值得
环球视点!一加Ace 2普及16GB超大内存:44个应用保活
世界观点:首批入手三星Galaxy S23用户开机后大跌眼镜:系统吃掉60GB存储空间
【天天速看料】【算法训练营day41】LeetCode343. 整数拆分 LeetCode96. 不同的二叉搜索树
今日观点!亚马逊商城入门教程_编程入门自学教程_菜鸟教程-免费教程分享
天天快资讯:计算机视觉——SSD和YOLO简介
焦点热议:CTF之WEB学习路线规划
当前焦点!第一个编译器
上海网红“安福路小公主”接代言引热议:网友感叹辣眼睛的大妈无敌了
全球播报:13代CPU+40系GPU 笔记本和台式机区别有多大?
微头条丨100%纯棉 班尼路短袖99元4件包邮:13色可选
今日热文:ChatGPT概念全球爆火!我国人工智能相关企业超百万家
没法出二手“回血”了!索尼称PS VR2初期将仅推出数字版游戏
视讯!火山引擎ByteHouse助力中国地震台网中心,快速构建一站式实时数仓
实战分享 | 金融数据采集报送平台实践
观天下!我是如何用CAP和BASE两个基础理论卷死其他组员的?
环球信息:【踩坑记录】@Transactional注解回滚不生效问题
世界微资讯!开心档-软件开发入门之MongoDB 覆盖索引查询
世界动态:89%美国大学生用写作业!揭秘爆红ChatGPT之父:应对核末日狂囤黄金、中学就出柜
当前快报:2023年首次更新!微信Mac版3.7.0发布:终于支持图片文字提取
世界观察:Github、Gitee优秀的开源项目
【ChatGPT 注册】
【天天聚看点】ChatGPT让苹果急了!消息称苹果将于下周召开内部AI峰会
环球快资讯丨299元!小米对讲机2S发布:市区5公里通话 100个对讲频道
热议:4个小号给大号浇水 缴电费再退骗蚂蚁森林能量被封号!法院判了
人工智能立大功!AI筛查阿尔茨海默病准确率达75%
每日视点!19999元!尼康发布尼克尔Z 85mm f/1.2 S镜头:模特发丝根根可数
观天下!从这些爆款小游戏中,你看到了什么?
Docker容器使用 (入门到精通)
HGAME_2023_WEB_WP_WEEK3
2799限量抢!小米Civi 2潮流版预售 网友:我一男生都表示太好看
世界报道:豆瓣仅5.7分 漫威超英大片《黑豹2》国内首映 开场票房就被《流浪地球2》超越
每日速递:男子花两千元套圈 套中两台宏光汽车:本人回应只要一辆 做人留一线
国产纯电两门跑车!哪吒E实车亮相:动力暴躁
焦点!百度类ChatGPT产品“文心一言”公开:股价应声上涨13%
诺基亚800c手机上市价格是多少?诺基亚800c手机参数
breeno指令是干什么用的?breeno指令在哪里?
戴尔笔记本电脑换电池大概多少钱?戴尔笔记本电脑开机黑屏没反应怎么办?
格力空调型号在哪里查看?格力空调型号一览表匹数
美的冰箱质量怎么样?美的冰箱怎么调温度?
天天视讯!Redis的十六种应用场景
全球短讯!一款备受欢迎的用户脚本管理器插件TampermonKey-油猴脚本管理器安装与使用
越狱最后各人物的结局是什么?电视剧越狱演员表
曾志伟的身高是什么?曾志伟个人资料
12月是什么星座?描写12月你好的优美说说
取关是什么意思?取关抖音好友对方知道吗?
期中考试家长意见怎么写?期中考试后的感想作文模板
热点评!19岁中国小伙确诊老年痴呆 全球纪录又被刷新了
全球看点:土耳其大地震 多家中国上市公司称影响有限
ChatGPT教你写AI包教包会!7段对话写出识别程序 准确度最高达99.7%
每日资讯:小米Civi 2潮流限定版来了:后盖上全是粉色Hello Kitty 超萌
世界看热讯:国产ChatGPT何时能出?百度站出来了:最快3月 评分全球第一
[西湖论剑2022]Misc-机你太美
焦点资讯:抖音视频无水印下载器,抖音视频下载器,下载未开放下载权限的视频,支持批量下载,同一作者作品自动批量下载,合集下载,话题挑战视频合集下载等
世界快消息!【2023微博签到爬虫】用python爬上千条m端微博签到数据
带了一个 3 年的开发,不会循环删除 List 中的元素,心态崩了。。
环球通讯!DataX插件二次开发指南
7斤纯铜就能压制酷睿i9 为啥还要散热风扇?原因一个字:贵