HashMap源码解析JDK1.8 —————— 开开开山怪

2024-02-07 07:48

本文主要是介绍HashMap源码解析JDK1.8 —————— 开开开山怪,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

hashMap源码解析

导航

1.hashMap的数据结构
2.影响hashmap性能因素
  • key的hash实现
  • loadFactor的设定
  • 数组capacity的设定
3.hashmap的主要方法解析
  • put(K key, V value)
  • get(Object key)
  • resize()
  • remove(Object key)
  • containsKey(Object key)

正题

1.hashMap数据结构

hashMap的主要数据结构为 数组 + 链表(红黑树)

在这里插入图片描述
链表节点

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的后驱节点引用}

红黑树节点

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 当前节点的父节点TreeNode<K,V> left;    //当前节点的左孩子TreeNode<K,V> right;   //当前节点的右孩子TreeNode<K,V> prev;    //当前节点的前驱节点boolean red;  //当前节点的颜色}

当数组中的每个bin中的节点构成的链表到达一定的长度,链表会转化为红黑树,但是在构成红黑树的同时不会破坏之前的链表的链接关系,只会将红黑树中的根节点在原链表中的位置和原链表的头节点进行替换。(后边会有详细的图解表明不会破坏之前的链表的关系,只是新增添了成员进行红黑树的转化)

2.影响hashMap的性能因素

hashMap成员:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // hashMap默认的数组容量为16
static final int MAXIMUM_CAPACITY = 1 << 30;//hashMap默认的数组最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子的值
static final int TREEIFY_THRESHOLD = 8;//每个bin中的链转化为红黑树的节点个数阀值
static final int UNTREEIFY_THRESHOLD = 6;//每个bin中从红黑树转化为链表的节点个数的阀值
static final int MIN_TREEIFY_CAPACITY = 64;//将bin中链表进行红黑树转化的数组最小容量,具体的意思就是,如果数组的容量<64,那么当bin中的链表节点个数超过8个,也不会进行红黑树的转化,而是采取扩容的手段。transient Node<K,V>[] table;  //主要存储结构,node数组
transient Set<Map.Entry<K,V>> entrySet;//
transient int size;//放入到数组中的元素的个数
transient int modCount;//容器的被修改次数,增删改都会对此变量加1
int threshold;//此变量是需要扩容的阀值,采用capacity*loadFactor得出final float loadFactor;//负载因子,当数组中的元素的个数大于capacite * loadFactor是需要进行数组的扩容,0.75为默认值。
1.loadFactor的设定

1. loadFactor:负载因子对于hashmap来说是其性能的关键因素之一
当loadFactor较小时,浪费的table空间较大,扩容较频繁,size的个数很快达到扩容的阀值。但好处是减小了冲突 (冲突是指不同的节点被放入同一个bin中的可能性),相当于每个bin中差不多只有一个节点,put操作和get操作代价较低,相当于是O(1);
2. 当loadFactor较大时 (loadFactor的值可大于1) ,这时候空间利用率较高,size的个数不会很快达到阀值,但同时带来的问题就是冲突增大,因为可容纳节点越多,被放入同一bin的可能性越大,而对之后的put和get的操作,可能就要进行遍历链表或红黑树,相对费时。

所以结合时间复杂度和空间利用率来看,0.75在这两反面来说相对均衡。

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

这里采用的key.hashCode右移16为再与其进行异或计算出最后的key的hash值,再采用这个hash进行与数组容量 - 1进行相与计算出节点应放入哪个下标的bin中。

原因: 我们在进行存储节点的时候采用的是key.hash &(capacity -1),就是根据key的hash值与数组的容量进行取余(hashMap采用位运算的方式key.hash &(capacity -1),运算速度更快),让节点随机落在数组的某个bin中,hash值相对数组的容量值来说大得多,所以进行位运算时,只有低位能用的上,而hash的高位的随机性会产生浪费,所以需要采用上边代码的方式,目的就是增加hash值的随机性,落入bin的随机性,进而减少冲突,提高hashMap的性能。

3.数组capacity的设定

补充:为什么table数组的容量要求为2的整数次幂
hashMap中采用这个方法保证,当用户给定一个容量的时候,能计算出比给定容量大的最小的2的整数次幂

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;
}

此方法,用来计算应该申请多大的数组,如果用户不设置size的话,则默认的size为16,
如果用户设置size的话,则最后的数组size大小,通过这个方法计算出一个大于用户给定size的最小数,这个数为2的整数次方
例如cap如果是5, 则n = 5 -1 = 0100
n |= n >>> 1 = 0110
n |= n >>>2 = 0111
n |= n >>> 4 = 0111
n |= n >>> 8= 0111
n |= n >>> 16 = 0111
return 0111 + 1 = 1000;

如果cap = 6 则n= 6-1 = 0101
n|= n>>>1 = 0111;
n|= n>>> 2 = 0111;
n |= n >>> 4 = 0111
n |= n >>> 8= 0111
n |= n >>> 16 = 0111
return 0111 + 1 = 1000

最后返回中的值都是比capacity大的最小的2 的整数次方
目的:同样也是为了减少节点落入bin中的冲突 ,因为决定节点落在哪个bin采用的是key.hash &(capacity - 1)计算出数组下标进行放入;那当capacity为2的整数次幂的话,capacity - 1的值采用二进制表示的话,除过最高位为0,后边的值都为1,那么此时随机性完全取决于hash的随机性,如果capacity采用的是奇数那么此时capacity-1就为偶数,那么capacity-1的值采用二进制表示的话有多位都是0的情况,那么再与hash进行相与的时候,完全抹杀了hash的低位某些位的随机性,不管hash值的低位的某些位是1还是0,最后相与0相与的结果都是零。同样这也就是hashMap为什么要求table的容量为2的整数次幂。

3. hashmap的主要方法解析

1.put(K key, V value)

put(K key, V value)->putVal(hash(key), key, value, false, true)

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
putVal方法

流程图
在这里插入图片描述

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//这个针对进行完初始化,首次进行put的时候,需要进行扩容操作。if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;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) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) //流程图这部分没有画,//这部分就是,当将新节点链入到链表的末尾时,//此时节点个数大于到达转化为红黑树的节点数阀值时 treeifyBin(tab, hash)方法先判断table的容量是否大于链表转化为红黑树的最小table容量即MIN_TREEIFY_CAPACITY = 64,这个值在最开始已经解释过了//如果不满足,那只进行扩容操作,如果满足,先组成链表的普通节点先转化红黑树节点,在将链表转化为红黑树的形式,//红黑树节点是在node节点上进一步增加了成员,在转化为红黑树的时候,之前node节所保持的链表采用next来链接的这种关系并未有抹杀,只是又增添了每个节点的父节点,左孩子节点,右孩子节点和前驱节点,以及该节点的颜色仅此而已treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//此处表明放入的键与原map中的键相等,只需更改值//onlyIfAbsent默认值是false,表示允许更改值if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);//这个方法再hashmap中并没有实现,但是在linkedHashMap中有实现,因为当linkedhashMap中将e!= Null看做是对map中的键值进行访问,相当于想要put的键map中已经存在,当做是访问看待,afterNodeAccess方法会将当前访问的这个节点移至链表的末尾,这里说的链表是在linkedhashMap中做维持的链表。return oldValue;}}//每次的put操作相当于修改了hashmap,修改此时加1.++modCount;if (++size > threshold)//当map中的节点个数大于扩容阀值,进行扩容操作。resize();afterNodeInsertion(evict);return null;
}
2.get(Object key)

get方法返回的值为null有两种情况
1.getNode(hash(key), key)的返回值为null,表明在map中没有该键,此时get方法返回null
2.getNode(hash(key), key)的返回值不为null,get的返回值为e.vaue.由于map中国允许键值都可以为null。所以也有可能存在该键,但值为null。

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode方法
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//首先table不为null,并且table的长度大于零,并且根据需要查找的key的hash值来确定节点应在table中的哪bin中,并且bin中拥有节点,才有继续查询的必要。@1.   if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//代码能到这里说明key的hash值经过计算做确定的bin中有节点//先判断第一个节点是否匹配@2      if (first.hash == hash && //第一个节点就相等直接返回该节点((k = first.key) == key || (key != null && key.equals(k))))return first;//代码到这里说明bin中有第一个节点,并且第一个节点不是匹配节点//先判断该节点是否有后驱节点//如果没有后驱节点,那么可以断定要查找的节点不存在于map中@3       if ((e = first.next) != null) {//代码到这里说明有后驱节点//然后进行链表和红黑树节点的判断,在根据不同情况进行处理if (first instanceof TreeNode)//如果bin中的数据结果属于红黑树,根据红黑树的查询方式进行查找,并返回return ((TreeNode<K,V>)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);}}//当map中不存在所要查找的键时候,返回null。return null;
}

在一开始展示了hashMap的基本结构,最开始的table数组的bin中的结构只是为了大家更清楚的知道,每个bin中可能存在链表,也可能是红黑树的结构,下面这个图更直观,(同样也解释了我之前说的,转变为红黑树结构后,并没有破坏链表的关系。)
在这里插入图片描述

3.resize()

对于hasnmap来讲,和ArrayList一样,也采用的是懒加载的模式,在进行初始化的时候并没有初始化table,只是简单的对负载因子,和扩容阀值进行设定,并且在初始化的时候如果采用的是带容量参数的构造方法进行初始化,扩容阀值=capacity,而不是capacity * loadFactor;真正的capacity * loadFactor的设定是在经构造方法初始化后,第一次进行put操作调用resize方法进行的。

resize 真正的用途有两处,同样也是resize调用的两种情况。
情况1 : 在进行初始化之后,第一次进行put操作,再对扩容阀值进行设定,并在此申请该有的数组空间。
情况2 : 当数组的元素个数大于扩容阀值,也是真正的由于数组容量不够进行扩容的情况。
由于 resize 代码太长,我拆分成两部分来说,可以对照下面源码进行理解

第一部分:扩容容量与扩容阀值的确定

第一部分的内容的目的就是计算出新的容量值,和新的扩容阀值,并且进行数组的申请。

1.resize调用情况一

1 :采用的是有initialCapacity参数的构造方法进行初始化之后

 int oldCap = 0;int oldThr = threshold = tableSizeFor(initialCapacity);int newCap, newThr = 0;不满足 if (oldCap > 0),满足else if (oldThr > 0),newCap = oldThr = tableSizeFor(initialCapacity);此时满足if (newThr == 0) newThr = newCap * loadFactor;将扩容阀值重新进行设定。

2采用的是无initialCapacity参数的构造方法进行初始化之后

 int oldCap = 0;int oldThr = threshold = 0;int newCap, newThr = 0;不满足 if (oldCap > 0),不满足else if (oldThr > 0),满足最后一个条件newCap = DEFAULT_INITIAL_CAPACITY = 16;newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY) = 16 * 0.75;
2.resize调用情况二

int oldCap = oldTab.length ;
int oldThr = threshold = 旧的capacity * loadFactor;
int newCap, newThr = 0;
满足if (oldCap > 0)

1. 如果满足if (oldCap >= MAXIMUM_CAPACITY) ,因为容量已经够大,直接将扩容阀值改为Integer.MAX_VALUE,不用再进行扩容,直接往table中添加节点。

2. 如果不大于hashMap默认数组的最大容量,进入 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
满足新的容量不大于最大容量,旧容量必须大于hashmap默认的数组容量。
此时对于采取的是二倍扩容法,新的容量为旧容量的二倍,并且扩容阀值也不采用负载因子进行计算,同样新的扩容阀值为旧的扩容阀值的两倍。
newCap = oldCap << 1;
newThr = oldThr << 1;

3. 如果上述条件不满足,说明进行二倍扩容后,容量大于hashmap默认的最大容量,或者旧的容量小于默认数组容量,则分为下面两种情况

@1. 表明扩容后容量大于hashmap默认数组最大容量
newCap = oldCap << 1;
newThr = Integer.MAX_VALUE;

@2. 表明扩容后不大于hashmap默认的数组最大容量,但是旧的容量小于数组默认容量16;
newCap = oldCap << 1
newThr = newCap * loadFactor;

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//先将table的 旧容量 和 旧阀值 记录。int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {//程序进到这个判断,说明是真的需要扩容,而不是在进行初始化之后的第一put时候进行数组的初始化。if (oldCap >= MAXIMUM_CAPACITY) {//当旧容量大于默认的table数组最大容量时,//直接让扩容阀值 = Integer.MAX_VALUE;//因为此时的容量早已超过MIN_TREEIFY_CAPACITY//threshold = Integer.MAX_VALUE;return oldTab;}//当旧容量不大于默认的数组最大容量,我们采用的是2倍扩容法//新的容量为旧容量的二倍,至于为什么是2倍,resize代码的第二部分会进行详细说明//newCap = oldCap << 1;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 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);}threshold = newThr;Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];   table = newTab;

此上的将resize调用后,新的数组容量值和扩容阀值的情况分析完毕。
下面则根据新的数组进行扩容后的操作(进行原数组中节点的挪动操作)

第二部分:链的拆分过程(解释为什么扩容为原数组容量的2倍)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {//遍历数组,每一个元素进行遍历,Node<K,V> e;if ((e = oldTab[j]) != null) {//下标为0的元素,判断下标为0是否有元素存在,如果存在,进入到下面,如果不存在则进入下一次循环,目的是找到数组的第一个有效节点开始进行遍历。//进入if表明当前数组下标中有元素oldTab[j] = null;//先令当前的下标中值为null,为之后会将当前下标中的链拆分的链重新放入该下标中做准备。if (e.next == null)//如果当前的下标中有元素,并且表明该元素没有后驱节点,则直接将该元素的hash和newCap - 1进行相与得到新数组的对应下标。newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)//如果该元素的链满足红黑树,则采用红黑树的方式进行将链进行处理((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 {//这一层else是在取出数组第一个节点,此时第一个节点不为null,并且第一个节点还有后驱节点的情况//我们需要将该节点所引的链中的节点部分挪到新的数组中next = e.next;//这个e代表的是从第一个有效节点的bin中取到的第一个元素,程序能到这里来说明此bin中是个链//先保存第一个节点的后驱节点,next为下一个节点也即后驱节点//下面if判断就是为了将原数组中某一下标的链中拆分为两个链,分别放入到新数组中下标为原数组的被拆分链所在的下标,或者为原数组被拆分链所在下标 + n。
//具体分析:
//本身我们通过e.hash &(n - 1)来确定将节点放在数组的那个坐标下,因为现在扩容的原因
//因为在扩容的过程中,采用二倍的方法进行扩容,
//举个栗子如果原来的数组的容量为16,那么节点所在下标为 hash & (16 -1)也相当于hash & 0000 1111
//当进行扩容的时候,容量扩大为2倍为32,那么之前在原数组中的节点在新数组的下标为 hash &(32 -1)相当于 hash & 0001 111
// 此时hash &(0001 1111)= hash &(0000 1111 | 0001 0000)=hash &(0000 1111)| hash &(0001 0000)
//hash &(0000 1111) 为原来数组中的下标
//hash &(0001 0000)为原来的容量n,拿现在的列子来说就是n = 16,
//对于扩容后对于原来数组中某一下标中元素在新数组中的下标就是:  原数组下标 + hash &(0001 0000)
//而对于 hash & (0001 0000)的结果只有两个,一个是0000 0000  一个是 0001 0000,总结来说,要不然是0 要不然就是n
//所以原来数组中的某一元素转移到新的数组中的下标 ,要么是原下标,要么是原下标 + n。此时的n就是capacity。if ((e.hash & oldCap) == 0) {//当前元素的hash值与n相与,目的为了判断该元素在新数组的位置到底是和原来下标相同是原来下标+n,//进入当前if判断表明(e.hash & oldCap)==0,表明当前正在遍历的节点在放入新数组的位置和原数组的位置相同(即下标相同的bin中)//下面的操作就是将本次遍历的原数组bin中的链表的部分节点构成一个新的链if (loTail == null)//因为在一开始的时候新数组中没有任何的元素,所以末节点为nullloHead = e;//此时将满足下标的第一个元素设置为头节点elseloTail.next = e;//程序到这一步,说明已经有第二个满足该下标的节点,将该节点连在末节点后边loTail = e;//每一轮的当前元素都是e,//这里用了个技巧,分为第一次情况和之后几次的情况,对于第一个情况来说,只设置并保存头节点
//对于之后的来说,只是将节点当前节点连接在末节点之后。}else {//这里的else情况表明(e.hash & oldCap)!= 0,表明当前正在遍历的节点在放入新数组的位置和原数组位置下标 + n的值相同(也即原数组下标 + capcaity 下标位置)//下面的操作就是将本次遍历的原数组bin中的节点满足应落在新数组中 原数组下标 + capcaity下标位置 的节点组成另一条链。if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//下面的两个if操作就是将上面分别形成的两条链放入到新数组不同的bin中//newTab[j] 表示与原数组下标相同的bin//newTab[j + oldCap] 表示与原数组下标 + n 相同的binif (loTail != null) {//因为lotail代表的是当前e,loTail.next = null;//让末节点的后驱节点设置为nullnewTab[j] = loHead;//并且将新链表的头节点放置在新数组的对应下标(该链在原数组中的下标)中,}if (hiTail != null) {//而这个则是在新数组中下标为原数组下标 + n处的下标放置原数组下标的链的部分节点hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}
}
return newTab;
4.remove(Object key)

一般容器的删除方法都会将删除的元素进行返回,但remove的返回值null同样有两种情况
1.没有找到要删除的键,返回为null
2.找到了要删除的键,返回该键的对应值,值可能为null。

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;//通过对象查找对象进行删除,首先确保想要删除的键在hashMap中存在,//通过键的hash中来判断数组中通过hash计算的下标中是否有节点存在,//存在才有继续寻找的必要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//若第一个节点不是要删除的节点,进入else ifif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//第一个节点就是要删除的节点,用node保存该节点node = p;else if ((e = p.next) != null) { //表明下标中第一个节点不是要删除的节点,继续判断,第一个节点是否有后驱节点//没有后驱节点 :直接跳出当期那else if,表明hashmap中没有对应节点要删除     //有后驱节点,才进入下面的if 和else的判断if (p instanceof TreeNode)//当前节点为红黑树节点,根据红黑树的查找删除规则进行,node 的值可能为null,可能为查找到的节点。node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {//当前是链表,根据链表的规则进行查找删除//node有两个是1.null 2.非null//node值为null说明没有要删除的节点,不为null说明存要删除的节点do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}//根据node的值来进行下一步操作//node为null,表明没找到要删除的键,不进入下面的if,直接返回null//node不为null,进入到下面的if,因为remove(Object)方法默认的matchValue 为false.如果是remove(Object, Object),matchValue 默认为true,,所以还需要根据值是否相等进行删除。if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode)//保存的node节点属于红黑树节点,按照红黑树的删除规则进行删除((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)//这里表示的意思是下标的中的第一个节点就是要删除节点,直接让该下标的节点为删除节点的后驱节点。tab[index] = node.next;else//保存的节点为普通节点,根据链表的形式进行删除p.next = node.next;++modCount;--size;afterNodeRemoval(node);//这个方法同样哎hashMap中没有实现//但是在linkedHashMap中有所实现。return node;}}return null;
}
5.ContainsKey(Object key)

这个方法事实上内部调用了getNode方法,getNode方法再上边已经有所讲述当hashMap中没有这个相同的key是返回值为null,ContainsKey通过返回的是否为null来判断是否拥有键。

public boolean containsKey(Object key) {return getNode(hash(key), key) != null;
}

这篇关于HashMap源码解析JDK1.8 —————— 开开开山怪的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/687021

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Centos7安装JDK1.8保姆版

工欲善其事,必先利其器。这句话同样适用于学习Java编程。在开始Java的学习旅程之前,我们必须首先配置好适合的开发环境。 通过事先准备好这些工具和配置,我们可以避免在学习过程中遇到因环境问题导致的代码异常或错误。一个稳定、高效的开发环境能够让我们更加专注于代码的学习和编写,提升学习效率,减少不必要的困扰和挫折感。因此,在学习Java之初,投入一些时间和精力来配置好开发环境是非常值得的。这将为我

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。