【数据结构-源码分析】HashMap源码分析(超级详细)
文章内容
- 1、HashMap简介
- 2、类结构
- 3、属性
- 4、构造方法
- 5、方法
- 5.1、put方法(新增)
- 5.2、resize方法(扩容)
- 5.3、get方法(遍历)
- 5.4、remove方法(删除)
- 5.5、treeifyBin方法(链表树化)
- 5.6、untreeify方法(红黑树转链)
- 6、jdk1.8的优化
1、HashMap简介
HashMap基于哈希表的Map接口实现,是以key-value存储形式存在。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同)。在 JDK1.8 中,HashMap 是由数组 + 链表 + 红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。当一个值中要存储到Map的时候会根据Key的值来计算出他的hash,通过哈希来确认到数组的位置,如果发生哈希碰撞就以链表的形式存储。当链表长度过长时,HashMap会把这个链表转换成红黑树来存储。
但是这样的话问题来了,HashMap为什么要使用红黑树呢,这样结构的话不是更麻烦了吗??
这个问题我也没有想过,其实很多在看的时候只会在乎红黑树的实现而忽略到了为什么要使用的这个问题,我也是在写本文的时候突发疑惑。参考了网上的例子,同时也解释了为什么阀值为8:
因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。至于为什么阈值是8?我想去源码中找寻答案应该是最可靠的途径。
2、类结构
我们来看一下类结构
在阅读源码的时候一直有个问题很困惑就是HashMap已经继承了AbstractMap而AbstractMap类实现了Map接口,那为什么HashMap还要在实现Map接口呢?同样在ArrayList中LinkedList中都是这种结构。
据 java 集合框架的创始人Josh Bloch描述,这样的写法是一个失误。在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。
Cloneable 空接口,表示可拷贝
Serializable 序列化
AbstractMap 提供Map实现接口
3、属性
/*** The default initial capacity - MUST be a power of two.*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认容量大小(必须是二的n次幂)/*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.*/static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量(必须是二的n次幂)/*** The load factor used when none specified in constructor.*/static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子(默认的0.75)/*** The bin count threshold for using a tree rather than list for a* bin. Bins are converted to trees when adding an element to a* bin with at least this many nodes. The value must be greater* than 2 and should be at least 8 to mesh with assumptions in* tree removal about conversion back to plain bins upon* shrinkage.*/static final int TREEIFY_THRESHOLD = 8; //当链表的值超过8则会转红黑树(1.8新增)/*** The bin count threshold for untreeifying a (split) bin during a* resize operation. Should be less than TREEIFY_THRESHOLD, and at* most 6 to mesh with shrinkage detection under removal.*/static final int UNTREEIFY_THRESHOLD = 6; //当链表的值小于6则会从红黑树转回链表/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.*///当Map里面的容量(即表长度)超过这个值时,链表才能进行树形化 ,否则元素太多时会扩容,而不是树形化 //为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD(因此树形化有两个条件,表长度 > 64 and 链表长度 > 8)static final int MIN_TREEIFY_CAPACITY = 64; /* ---------------- Fields -------------- *//*** The table, initialized on first use, and resized as* necessary. When allocated, length is always a power of two.* (We also tolerate length zero in some operations to allow* bootstrapping mechanics that are currently not needed.)*/transient Node<K,V>[] table; // table用来初始化,类似容器来存放元素(必须是二的n次幂)/*** Holds cached entrySet(). Note that AbstractMap fields are used* for keySet() and values().*/transient Set<Map.Entry<K,V>> entrySet; // 用来存放缓存/*** The number of key-value mappings contained in this map.*/transient int size; // Map中存储的元素数量/*** The number of times this HashMap has been structurally modified* Structural modifications are those that change the number of mappings in* the HashMap or otherwise modify its internal structure (e.g.,* rehash). This field is used to make iterators on Collection-views of* the HashMap fail-fast. (See ConcurrentModificationException).*/transient int modCount; // 用来记录HashMap的修改次数/*** The next size value at which to resize (capacity * load factor).** @serial*/// (The javadoc description is true upon serialization.// Additionally, if the table array has not been allocated, this// field holds the initial array capacity, or zero signifying// DEFAULT_INITIAL_CAPACITY.)int threshold; // 阈值,用来调整大小下一个容量的值(计算方式为容量*负载因子)/*** The load factor for the hash table.** @serial*/final float loadFactor; // 负载因子,创建HashMap也可调整,比如你希望用更多的空间换取时间,可以把负载因子调的更小一些,减少碰撞。
4、构造方法
HashMap()
构造一个空的 HashMap ,默认初始容量(16)和默认负载因子(0.75)。HashMap(int initialCapacity)
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。HashMap(int initialCapacity, float loadFactor)
构造一个空的 HashMap具有指定的初始容量和负载因子。我们来分析一下。
最后落到tableSizeFor方法,后面讲下;HashMap(Map<? extends K, ? extends V> m)
通过旧的Map来创建新的HashMap对象。
tableSizeFor(扰动函数)解析
这是HashMap源码中的一个方法,这个方法的作用是找到一个大于或等于cap最近的2的n次方数
例:cap = 14,return 16;cap = 76,return 128;
static final int tableSizeFor(int cap) {//-1可以保证当传入的数刚好是2的次方时,可以正确的返回其本身,例:传入的是16,经过下面的计算后还是返回16int 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;}
先解释一下|=、>>>这两个运算符。
- 运算符|=
n |= n 等同于 n = n | n;
| 是位运算符(或) - 运算符>>>
>>>是无符号右移运算符
就是把一个二进制数右移指定位数,正数高位补0,负数高位补1;
例:
int num = ; //00000000 00000000 00000000 10111101
//无符号右移1位
num = num>>> = 1;//00000000 00000000 00000000 01011110
//把上一步得到的num再做无符号右移2位运算得到
num = num>>> = 2;//00000000 00000000 00000000 00010111
了解了以上两个运算符的作用,应该就能初步看明白源码中的tableSizeFor方法了吧。但是大部分人第一次看的时候应该都是一脸懵逼的,颇有一种我明明都看明白了每步做了什么,为什么合起来就看不懂了的感觉。下面来解释一下。
先思考一个问题:
如果给我们一个二进制数cap = 00000000 00000000 00000000 10111101这个二进制数的最近的一个2的n次方数是多少呢?学过二进制,我们可以应该可以一眼看出来这个数是cap = 00000000 00000000 00000001 00000000
我们一眼看出来了,但是程序不行,所以要思考一种办法能通过代码找到这个数。
假设我们可以把最高位1以及其后面的位都置为1,然后加1是不是就能实现这个功能了,即:
步骤1、cap = 00000000 00000000 00000000 11111111
步骤2、cap = cap + 1 = 00000000 00000000 00000001 00000000
tableSizeFor就是通过这两个步骤实现的
static final int tableSizeFor(int cap) {//-1可以保证当传入的数是2的次方时,可以正确的返回其本身,例:传入的是16,经过下面的计算后还是返回16//步骤一int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;//步骤二n+1return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}
这个代码的作用就是把最高位的1后面的所有位都置为1,而且从始至终其实只用到了最高位1这1位数字参与运算,非常的巧妙,膜拜。下面由一个例子来解释一下:
有个int数n,二进制为
n = 00000000 00000001 10111010 000001101;
现在我们只看最高位的1,1后面的位数用x代替,它是0还是1都不重要(后面你会发现确实不重要),表示为:
n = 00000000 00000001 xxxxxxx xxxxxxxx;把n做无符号右移1位运算并将结果赋值给m:
m = 00000000 00000000 1xxxxxxx xxxxxxxx;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 1xxxxxxx xxxxxxxx;把n做无符号右移2位运算并将结果赋值给m:
m = 00000000 00000000 011xxxxx xxxxxxxx;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 111xxxxx xxxxxxxx;把n做无符号右移4位运算并将结果赋值给m:
m = 00000000 00000000 0001111x xxxxxxxx;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 1111111x xxxxxxxx;把n做无符号右移8位运算并将结果赋值给m:
m = 00000000 00000000 00000001 1111111x ;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 11111111 1111111x ;把n做无符号右移16位运算并将结果赋值给m:
m = 00000000 00000000 00000000 00000001 ;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 11111111 11111111 ;有没有发现到不知不觉就把最高位1后面的位数全部都变成1了,而且从始至终其实只有最高位的1以及通过这个1计算得到的数参与了运算,
前面被我们表示为x的位根本没有用到。
因为java中int类型是32位的,所以5次无符号右移刚好能覆盖到32位数。同理如果是8位数只需要位移到4,如果是16位数只需要位移到8,,64位数则需要位移到32。n |= n >>> 1;//到这一步最多可以得到2位1n |= n >>> 2;//到这一步最多可以得到4位1n |= n >>> 4;//到这一步最多可以得到8位1n |= n >>> 8;//到这一步最多可以得到16位1n |= n >>> 16;//到这一步最多可以得到32位1
把得到的n加1就能得到了一个最近的大于n的2的次方数。
但这块有个疑问,该方法生成的值为啥赋给threshold??希望路过的大佬给解答下。
5、方法
5.1、put方法(新增)
我们可以看到put调用的是putVal来进行数据插入,但是要注意到key在这里执行了一下hash()方法,来看一下Hash方法是如何实现的。
理论上来说字符串的hashCode是一个int类型值,那可以直接作为数组下标了,且不会出现碰撞。但是这个hashCode的取值范围是[-2147483648, 2147483647],有将近40亿的长度,谁也不能把数组初始化的这么大,内存也是放不下的。
默认初始化的Map大小是16,所以获取的Hash值并不能直接作为下标使用,需要与数组长度进行取模运算得到一个下标值。 HashMap源码这里不只是直接获取哈希值,还进行了一次扰动计算,(h = key.hashCode()) ^ (h >>> 16)。把哈希值右移16位,也就正好是自己长度的一半(int类型32位的一半),之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。
说白了,使用扰动函数就是为了增加随机性,让数据元素更加均衡的散列,减少碰撞。
从上面也可以得知HashMap是支持Key为空的,而HashTable是直接用过Key来获取HashCode所以key为空会抛异常。因为HashMap 使用的方法很巧妙,它通过 hash & (table.length -1)来得到该对象的保存位(数组下标),前面说过 HashMap 底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当 length 总是2n次方时,hash & (length-1)运算等价于对 length 取模,也就是 hash%length,但是&比%具有更高的效率。比如 n % 32 = n & (32 -1)。
现在再来看看需要验证的问题,以及putVal方法的实现:
- 验证容器懒加载(put元素后分配空间)√
- 正常put元素(index位置元素存在及不存在的情况)√
- 容器扩容 √
- 链表转红黑树√(红黑树的数据结构暂时不太了解,实现方法看不懂,后面更)
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,table 被延迟到插入新数据时再进行初始化 if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 空表进行首次扩容初始化tableif ((p = tab[i = (n - 1) & hash]) == null) // i位置上元素不存在(通过i=(n-1)&hash生成元素的数组下标)tab[i] = newNode(hash, key, value, null); //结点不存在,就创建新的Node结点放到i位置else { // i位置上元素存在Node<K,V> e; K k;// 如果i位置上原结点的hash值与待插入的一样,且key也一样,就是值的更新操作,进行覆盖if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode) // 红黑树结点,就调用特有的putTreeVal方法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); // 判断是否进行树化,TREEIFY_THRESHOLD是8,表明// 当链表插入第9个元素后,进行树化操作,但treeifyBin方法又判断tab长度需 >= 64if (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; // 向链表尾递推}} // 如果链表头结点或遍历链表过程中某个结点,满足p.hash == hash // &&((k = p.key) == key || (key != null && key.equals(k))),就进行更新操作,同时返回oldValue值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;}
5.2、resize方法(扩容)
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// Cap 是 capacity 的缩写,容量。如果容量不为空,则说明已经初始化。 if (oldCap > 0) {// 规定扩容上限就是 1 << 30if (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 thresholdnewCap = oldThr;else { // zero initial threshold signifies using defaults// 第一次put,由于是无参创建的map会直接用默认的容量及负载因子,同时计算阈值newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) { // 阈值为0,则按照tableSize*loadFactor计算出来float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 赋值新阈值、新表大小threshold = newThr;Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 将原Map中元素迁移到新Mapif (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; // 元素重新rehash下标,并赋值到新Mapelse if (e instanceof TreeNode) // 红黑树结点,就调用特有的split方法((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;// 将高低位区分处理:// 1、低位的loHead和loTail放的桶位置不变// 2、高位的loHead和loTail放的桶位置变成原索引+旧容量if ((e.hash & oldCap) == 0) { // e.hash & oldCap 等价于 e.hash % (oldCap + 1) ,例如16位扩容// 到32位大小的容器中,则0-15位的则计算为0,否则非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);// 将某index下的单个链表区分为高位链(hi)及低位链(lo)// 然后低位原位置不动,高位位置取oldCap + 原indexif (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
5.3、get方法(遍历)
public V get(Object key) {Node<K,V> e;// 同样需要经过扰动函数计算哈希值 return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&// 从上面分析到(n - 1) & hash])是根据hash值获取该map的下标位置(first = tab[(n - 1) & hash]) != null) {// key对应的val存在// 再次比较hash值以及keyif (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {// TreeNode 节点直接调用红黑树的查找方法,时间复杂度O(logn) if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 非TreeNode就遍历链表查找do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;}
红黑树的查找方法
5.4、remove方法(删除)
public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;}final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;// 前半部分代码与get方法类似,因为删除前需要先查找到待删除元素的位置if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {if (p instanceof TreeNode)node = ((TreeNode<K,V>)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<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)// 匹配到待删元素后,链表后一个结点直接进行覆盖,完成链表删除tab[index] = node.next;else // 该情况不太清楚?可能是p是定位到待删节点(node)的前驱结点p.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null;}
5.5、treeifyBin方法(链表树化)
5.6、untreeify方法(红黑树转链)
6、jdk1.8的优化
1)组成结构的变化
由JDK1.7的,数组 + 链表
JDK1.8变为:数组 + 链表 + 红黑树
具体触发条件为:某个链表连接的个数大于8,并且总的容量大于64的时候,那么会把原来的链表转换成红黑树。这么做的好处是什么:红黑树除了添加元素外,查询和删除效率比链表快。
红黑树查询、增加和删除的时间复杂度:O(log2n)
链表的查询和删除的时间复杂度: O(n),插入为:O(1)
从上图可以看出,随着元素插入的增加,红黑树查询、增加和删除的时间复杂度的时间复杂度都是适中的,且后面耗时逐渐平缓。但链表确实等比上升。
2)扩容元素拆分
拆分元素的过程中,原jdk1.7中会需要重新计算哈希值,但是到jdk1.8中已经进行优化,不在需要重新计算,提升了拆分的性能,设计的还是非常巧妙的。
随机使用一些字符串计算他们分别在16位长度和32位长度数组下的索引分配情况,发现原哈希值与扩容新增出来的长度16,进行&运算,如果值等于0,则下标位置不变。如果不为0,那么新的位置则是原来位置上加16。这样一来,就不需要计算每一个数组中元素的哈希值了。
3)扩容死循环问题
在JDK1.8以后,由头插法改成了尾插法来避免死链问题的产生。此问题造成的原因先不做讨论,感兴趣可以参考
【数据结构-源码分析】HashMap源码分析(超级详细)相关推荐
- 最最最详细的springboot项目中集成微信扫码登入功能.步骤代码超级详细(OAuth2)
说到登录注册,就会想到先要注册一个用户名,在进行登入,但是现在大多数的网站都集成了微信登入,不需要注册,给你一个二维码,微信一扫直接登录.这确实是十分便捷的.所以我们会尽量在项目中实现这一功能.减少用 ...
- HashMap 源码深度分析
HashMap 源码分析 在Map集合中, HashMap 则是最具有代表性的,也是我们最常使用到的 Map 集合.由于 HashMap 底层涉及了很多的知识点,可以比较好的考察一个人的Java的基本 ...
- 搞懂 Java HashMap 源码
HashMap 源码分析 前几篇分析了 ArrayList , LinkedList ,Vector ,Stack List 集合的源码,Java 容器除了包含 List 集合外还包含着 Set 和 ...
- 【JAVA进阶】java中的集合(番外篇3)- HashMap源码底层数据结构分析
写在前面的话 脑子是个好东西,可惜的是一直没有搞懂脑子的内存删除机制是什么,所以啊,入行多年,零零散散的文章看了无数,却总是学习了很多也忘了很多. 痛定思痛的我决定从今天开始系统的梳理下知识架构,记录 ...
- HashMap源码实现分析
点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 一.前言 HashMap 顾名思义,就是用hash表的原理 ...
- JDK 1.6 HashMap 源码分析
前言 前段时间研究了一下JDK 1.6 的 HashMap 源码,把部份重要的方法分析一下,当然HashMap中还有一些值得研究得就交给读者了,如有不正确之处还望留言指正. 准备 需要熟悉数组 ...
- HashMap 源码详细分析(JDK1.8)
1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...
- Java8 HashMap源码分析
前言 今天,我们主要来研究一下在Java8中HashMap的数据结构及一些重要方法的具体实现. 研究HashMap的源代码之前,我们首先来研究一下常用的三种数据结构:数组.链表和红黑树. ...
- 查询已有链表的hashmap_源码分析系列1:HashMap源码分析(基于JDK1.8)
1.HashMap的底层实现图示 如上图所示: HashMap底层是由 数组+(链表)=(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...
最新文章
- 利用LeNet识别十种动物和水果
- 空指针、悬空指针、野指针
- gprMax 3.1.5 安装以及在pycharm或VScode中运行的方法
- 遍历列表python_python中列表的遍历
- securecrt 中文横着显示解决
- PIC单片机开发工具
- 拓端tecdat|R语言Bass模型进行销售预测
- Stream Processing With Flink (7) 状态算子和用户函数
- 穿透还原卡和还原软件的代码
- 三阶科赫雪花PYTHON
- python中ipo是什么意思呢_ipo是什么意思呢?ipo是不是就是上市呢?
- HTML / CSS / JS 编程入门 —— 使用 Lightly 制作可切换主题的简单网页
- HTML5七夕情人节表白网页制作【浪漫森林落叶钢琴紫色3D相册】HTML+CSS+JavaScript
- 练习4.圆中四只鸭子在同一个半圆的概率——MATLAB
- Ubuntu20.4终端指令 Linux安装WPS2019
- NBU 配置ORACLE备份
- 家长叫我别天天我在房间没事多看看新闻,我说我马上写个爬虫爬新闻看!!!
- 算法打卡Day16_leetcode _94. 二叉树的中序遍历
- VSCode查看和编辑远程服务器的代码
- [转载]JSP与EJB
热门文章
- python-scrapy爬虫框架爬取王者荣耀英雄皮肤图片和技能信息
- 前端:LayUi监听表格单元格,编辑后恢复原数据
- 《西游记》取经路线图|思维导图简单又漂亮
- Blender 插件开发 将object设置成bpy.context.object
- 分享课丨微软研究院资深主任研究员郑宇教授:多源数据融合与时空数据挖掘(一)...
- 信息化,不只是技术-某公司局域网改造实例(转)
- 天兔(Lepus)监控操作系统(OS)安装配置
- Flash Player For Android
- 字符串转换成UTF8编码
- Work20230417