java ArrayList源码分析(深度讲解)
创始人
2024-06-02 23:44:51
0
  • ArrayList类的底层实现

  • ArrayList类的断点调试

  • 空参构造的分步骤演示(重要)

  • 带参构造的分步骤演示


一、前言

大家好,本篇博文是对单列集合List的实现类ArrayList的内容补充。之前在List集合的万字详解篇,我们只是拿ArrayList演示了List接口中的常用方法,并没有对它进行深究。但这正是我们今天要做的内容。
up会利用断点调试(Debug)来一步一步地给大家剖析ArrayList底层的扩容机制到底是如何实现的。空参构造和带参构造初始化ArrayList对象up都会演示到。但是,重点是空参构造器构造ArrayList对象后,底层扩容机制的详细实现。
注意 : ①解读源码需要扎实的基础,比较适合希望深究的同学;②不要眼高手低,看会了不代表你会了,自己能全程Debug下来才算有收获;③点击文章的侧边栏目录或者前面的目录可以进行跳转。④本篇博文对ArrayList源码的解读基于JDK17.0的版本,虽然不是主流的JDK8.0,但是经过对比不难发现其底层原理大同小异,所以,就算你用的不是高版本的JDK,本文也对打牢你的基础有一定帮助。良工不示人以朴。感谢阅读!

二、ArrayList类的底层实现

1.ArrayList类在底层是由数组来实现的,ArrayList类源码中维护了一个Object类型的数组elementData,用于存储ArrayList集合中的元素。

关于transient关键字,transient本身是转瞬即逝的意思,如下 :

被transient关键字修饰的程序元素不可被序列化。

2.当我们使用空参构造来创建ArrayList类对象时,则elementData数组的初始容量为0,第一次添加元素时,将该数组扩容为10,如需再次扩容,则将elementData数组的当前容量扩容为1.5倍。

3.如果使用指定数组初始容量大小的带参构造来创建ArrayList类对象,则elementData数组的初始容量即为传入形参的指定容量,如果需要扩容,则直接将该数组当前容量扩容至1.5倍。


三、ArrayList类的断点调试

0.准备工作 :

up以一下代码为演示,来进行Debug操作,(分别在第13行和第20行设置断点),代码如下 :

package csdn.knowledge.api_tools.gather.list;import java.util.ArrayList;/*** @author : Cyan_RA9* @version : 21.0*/
public class ArrayList_Demo {public static void main(String[] args) {//演示 : Debug ArrayList空参构造,以及数组扩容的全流程。//1.通过空参构造创建一个ArrayList类对象ArrayList arrayList = new ArrayList();System.out.println("使用空参构造创建的集合对象 = " + arrayList);//2.利用for循环向集合对象中添加10个元素。(0~9)for (int i = 0; i < 10; i++) {arrayList.add(i);   //此处有自动装箱}System.out.println("添加十个元素后,当前集合 = " + arrayList);//3.如果按照我们的理论,在向集合对象中添加第11个元素时,底层的数组需要扩容。(1.5倍)arrayList.add("这是集合的第十一个元素捏.");arrayList.add("这是集合的第十二个元素捏.");arrayList.add("这是集合的第十三个元素捏.");arrayList.add("这是集合的第十四个元素捏.");arrayList.add("这是集合的第十五个元素捏.");//4.再次测试ArrayList类底层数组的扩容机制。    (10 ---> 15 ---> 22)arrayList.add("这是集合的第十六个元素捏.");System.out.println("添加十六个元素后,当前集合 = " + arrayList);}
}

1.空参构造——分步骤Debug(详细阐释)(重要)

0°开始Debug。

首先,我们进入Debug界面,并在无参构造调用行(此处为第13行),跳入ArrayList类无参构造,如下GIF图所示 :

1°初始化底层elementData数组为空数组。

跳入ArrayList无参构造,我们可以看到,它将用于存储集合元素的elementData数组进行了初始化。等号后面的这一大堆直译过来就是 "默认容量空的elementData数组" ,可以根据Ctrl + b/B查看ArrayList中该常量的源码,如下 :

可以看到,这个所谓的 "默认容量空的elementData数组" 确实名副其实,真是一个空数组,而且与ArrayList中用于存储集合元素的elementData数组一样都是Object类型。

接下来,我们跳出这个无参构造,进入for循环,并跳入第一个元素的add方法。

2°对add方法中的实参进行自动装箱。

第一次跳入add方法,会跳到valueOf方法中,对要添加的int类型进行装箱操作。如下图所示 :

我们不用管他,直接选择跳出,并准备第二次跳入add方法。

第一次跳出add方法后,该行代码仍然标注着高亮,表示此行代码还未执行完毕。我们第二次跳入add方法。

3°进入add方法底层。

第二次跳入add方法,我们来到了真的"add"方法,如下 :

其中,形参列表的"e"代表了你要添加的元素,根据我们上面给出的代码,for循环要向集合对象中添加0~9这十个元素,这是for循环第一次循环,要添加元素0,因此可以看到,此时 e = 0。

modCount属性用于保存你修改集合的次数,用来防止有多个线程修改它,多个线程修改时会抛出异常,这里我们不用管它。

可以看到,跳入的add方法中,还调用了一个形参列表不一样的"add"方法,我们暂时将这个"add方法中的add方法"称为内层add方法,内层add方法传入了三个形参,分别是"当前要添加的元素(此时e = 0),"初始化后的空数组elementData", 以及"当前集合中的元素个数size"。显然,内层add方法更加底层,当然,我们要追进去看看。

4°进入grow方法。

更底层的add方法(内层add方法),如下 :

可以看到,内层add方法中首先是一个if条件语句的判断,if条件语句结束后,才将e(当前e = 0)添加到了elementData数组中,并且为size属性 + 1(集合中的元素个数从0 --> 1,多了1个)。

但是,要知道我们的elementData数组之前可是初始化为了一个空数组阿,是啥都放不下的。显然,它在if条件语句中被动了手脚。我们来仔细看看这个if条件语句,判断条件是"当前集合中的元素个数是否等于数组的大小(长度)", 啥意思呢?就是说咱们现在不是正要向集合中添加元素么——(前面我们也看了,向ArrayList集合中添加元素,底层其实就是向ArrayList类中的elementData数组中添加元素)——如果判断条件成立,说明当前集合的元素个数与底层elementData数组的大小相等,也就是说数组已经满了,再想添加元素就要扩容🌶!

我们再回到实际,因为我们正在添加第一个元素,所以目前集合中元素的个数 = 0;底层数组为空,所以目前数组的大小 = 0,0 = 0,满足判断条件。所以,肯定要进入这个grow方法。grow方法,见名知意,就是对数组进行扩容的方法。"elementData = grow();",显然grow方法最终是返回一个Object类型的数组,这样才能赋值给elementData数组(其实就是更改了elementData的引用)。我们也是一路高歌,继续跳入grow方法中看看是咋回事。如下图所示 :

果然,grow方法返回了一个Object类型的数组。不过比较操蛋的是grow方法与前面的add方法类型,都™长了层包皮,没关系,我们继续追进去看看。(注意 : 既然外层的grow方法是返回Object类型的数组,说明return后面的grow方法肯定要是返回一个Object类型的数组。不过,内层的grow方法传入了一个形参是size + 1,size表示当前集合中元素的个数,第一个元素还没加进去,所以size + 1 = 0 + 1 = 1.)

5°进入grow方法底层。

内层的grow方法如下图所示 :

是不是傻眼了😂?什么玩意儿!别急,不要因为走得太远而忘记了我们为什么出发。我们一路追追追,只有一个目的——elementData数组目前为空,要添加第一个元素0进入,必须先扩容,而内层grow方法要返回一个Object类型的数组。所以,现在我们就找内层grow方法中有扩容操作的代码就行,别的暂时不看!

可以看到,首先第一条语句,是把当前数组的长度(0)赋值给了oldCapacity变量,这条语句有啥用呢,我们暂时不管。继续往下看,有一个if-else的多重条件语句,判断条件是"当前数组的长度是否大于0,或当前数组是否不为空",显然,elementData数组目前还是个空呢,两个条件均不满足,于是if语句中的内容现在不执行,直接跳到else语句。

else语句中,注意观察,出现了return语句!显然,这里很可能就是返回Object类型数组的地方。我们来看看是不是。

返回的Object数组的长度,是调用Math类的max函数的返回值,我们之前在常用类中讲过Math类,max函数可以返回两个数中的较大值。第一个数"DEFAULT_CAPACITY",直译过来就是"默认的容量",其实不用我说你们也能猜出来,和我们一开始初始化elementData数组时遇到的那"一大堆"一个拉撒,一丘之貉,这里也不追进去看了,但是注意,当我们鼠标悬停在这个常量上面是,会显式它的值 = 10;第二个数是minCapacity(最小容量),这个变量是哪里来的呢?欸,你往上翻翻看,不就是内层grow函数的形参么!那这个形参是哪儿来的呢?欸,瞧你这记性,翻到上面外层grow函数,可以看到当时我们传入的是(size + 1),即0 + 1, 等于 1。minCapacity表示——要把该元素添加进去,所需数组的最小长度,我们这才添加第一个元素阿,那minCapacity当然是1了。那两个数中,显然10 > 1,内层grow函数最终返回的就是"new Object[10]"。

6°逐层返回,第一次扩容elementData数组完毕(0 ——> 10)。

然后,内层grow函数执行完毕,返回外层grow函数,如下 :

然后,再返回内层add函数,如下 :

接着,内层add函数中,if条件语句中的方法将要执行完毕。

可以看到,右上角的提示信息中,已经由原来的"Object[0]" 变成了"Object[10]",这也可以验证我们上文"ArrayList类的底层实现"中提到的——当我们使用空参构造来创建ArrayList类对象时,则elementData数组的初始容量为0,第一次添加元素时,将该数组扩容为10。

接着往下执行,可以看到第一个元素0已经被添加进了elementData数组,如下图所示 :

内层add函数执行完毕,返回外层add函数,如下 :

可以看到,size(当前集合中元素的个数)已经由0变成了1,说明我们添加成功了。接着返回到我们的测试类中,如下 :

7°向集合添加第二个元素(不需要扩容)。

第一次扩容完成后,elementData数组的长度由0扩到了10,因此,for循环中后续几个元素的添加都不再需要扩容。以第二个元素的添加为例,up再来带大家过一遍,加深印象,当然,这次主要是体验一下流程,不会像第一次一样那么细了。

如下图所示,随着循环变量i的自增,for的第一次循环顺利结束,第二次循环开始,向集合中添加第二个元素1 :

可以看到,首先还是老规矩,先将int类型的数据1做了装箱处理。

接着,我们再次跳入外层add方法,如下图所示 :

注意看右上角,e(即我们要添加的元素)已经是1了,因为代码里的for循环中,就是要向集合添加0~9这十个元素,现在是第二个元素1。继续往下执行,我们跳入内层add方法,如下图所示 :

首先注意看右上角,s变量(当前集合中的元素个数)变成了1,因为我们之前已经添加过1个元素了么。执行内层add方法,if条件判断语句中,当前集合中元素的个数显然小于elementData数组的长度(1 < 10),因此不进入grow方法,而是直接将第二个元素放入elementData数组中索引为1的位置,同时再对size变量进行加1操作。如下图所示 :

可以看到,第二个元素也已经成功添加到了elementData数组中,而且size变量也变成了2。

之后,依然是先跳出内层add方法,再跳出外层add方法,一直跳回去,如下GIF所示 :

8°将集合中的元素添加到10个元素。(第一个临界点)

之后的8次循环均与第二次循环相同,我们直接一笔带过,如下GIF图所示 :

9°集合的第二次扩容开始。

因为第一次对elementData数组扩容,默认只从0扩到了10。而for循环结束后,我们已经向集合对象中添加了十个元素,即ArrayList底层的elementData数组已被装满。现在我们想添加第十一个元素,就需要对elementData数组进行第二次扩容了。

我们还是先跳入add方法,看看会发生什么,如下图所示 :

可以看到,由于从第十一个元素开始,均为String类型,因此不需要"装箱"的操作,而是直接跳到了外层add方法。同样地,我们继续跳入内层add方法,如下图所示 :

注意看内层add方法中的if条件语句,IDEA已经给出了提示" = true",没错,当前集合中已有的元素个数s = 10,而当前elementData数组的长度 = 10,10 = 10,满足判断条件。因此这时候要第二次进入grow方法对数组进行扩容了。好,我们跳入外层grow方法,如下图所示 :

注意看这时内层grow方法的实参,minCapacity : size + 1,即要想把第十一个元素放入集合中,所需集合的最小容量是size + 1 = 11,即所需elementData数组的最小长度是11。好的,接下来我们跳入内层grow方法,如下 :

仍然是先将当前elementData数组的长度(10)赋值给了oldCapacity变量;接着,if条件语句,判断条件是"当前数组的长度是否大于0,或当前数组是否不为空",与我们第一次扩容不同,第一次是从0 ——> 10,现在elementData数组已经不为空了。因此,可以看到IDEA也是再次给出了提示" = true"。

接着往下走,if语句体中,我们发现它由两部分构成。

首先,第一部分,是一个newCapacity局部变量的定义,见名知意,显然它代表了我们内层grow方法要返回的新数组的长度。而为newCapacity赋值的是另一个底层方法newLength,我们先不管它,继续往下看。

第二部分,是一个return语句,显然,这个return语句中要返回扩容后的Object类型的新数组。

好的,框架清晰后,我们再看细节。既然第一部分要获取扩容后新数组的长度,那么显然我们是要追进去这个底层方法看看的。但是,进去之前,我们不妨先来看看它的形参列表,一共传入了三个数——①当前数组的长度;②所需新数组的最小长度 - 就数组长度(= 数组的最小增长量);③旧数组右移1位。这里稍微说一下这个"oldCapacity >> 1"是什么意思,其实涉及到了C语言的一些基础——位运算。这里面涉及到了二进制的知识,当然我们也就废话少说,直接告诉你结论:"<<"表示左移,每左移1位相当于*2;">>"表示右移,每右移一位相当于/2。那么此处的"oldCapacity >> 1"就表示将旧数组的长度的值(10)右移一位,相当于10 / 2,= 5。因此,此处调用的newLength方法的实参就分别为①10;②1;③5。

OK,搞清楚这些后,我们追进去newLength方法一探究竟,如下图所示 :

可以看到,newLength方法内部的结构还是比较清晰的 : 一个变量的赋值语句,一个if-else的复合条件语句。我们一个一个来看。

首先,"prefLength",直译过来就是"预设长度"的意思,见名知意,显然它和我们新数组的长度关系密切。继续看代码,为prefLength赋值的是"旧数组的长度 + 数组最小增长量和prefGrowth之间的最大值"。这个prefGrowth不知道大家能不能想到,前面我们传入三个实参的时候,第三个实参不是"oldCapacity >> 1"吗?欸,就是它!我们也可以将鼠标悬停在它上面,IDEA会显示出它的值,如下图所示 :

可以看到,确实 = 5,和我们前面推理的一致。而minGrowth和prefGrowth一个1,一个5,肯定5大呀。所以,最终赋值给prefLength的值 = 10 + 5 = 15。欸,有没有发现,正好验证了我们前文中所提到的——当我们使用空参构造来创建ArrayList类对象时,如需再次扩容,则将elementData数组的当前容量扩容为1.5倍。估计到这里大家也能理解为什么是1.5倍了,就和前面那个位运算">> 1"有关,本身再加上它的一半,可不是1.5倍么。

继续往下执行,if条件语句中,要判断新数组的长度是否合法,要满足大于0并且小于一个"一大堆"的玩意儿。我们还是将鼠标悬停在那"一大堆"上,看看它的值是多少,如下所示 :

哎呀我去,行了,不用看了,咱就一个测试的数组,能跑那么大吗?

🆗,newLength方法结束,返回内层grow方法。如下图所示 :

还是那句话,不要因为走得太远而忘记了我们为什么出发。newLength方法只是为了获取新数组的长度,我们也进去看了,也知道是个啥了,就是旧数组的1.5倍。最后返回新数组,还是内层grow方法来完成。

此处用到了Arrays的copyOf方法。copyOf方法大家不用深究,只需要知道它的功能是将原数组中指定长度的内容拷贝到新数组中,并且,若指定的长度大于原数组长度,则多出来的部分以默认值填充,最终返回的是新数组。使用该方法扩容数组,可以在保留原数组中内容的同时,又达到扩容的目的。此时,我们新数组的长度15显然大于原数组长度10,因此我们猜测,最后返回的数组的效果就是[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, null, null, null, null, null]。(第11个元素未加入前)

我们接着Debug,跳出内层grow方法,如下所示 :

接着再跳出外层grow方法,如下 :

在内层add方法中,我们继续执行,如下如所示 :

可以看到,扩容后elementData数组的情况与我们预测的一模一样。多出的4个元素均以默认值null取代了。而且,当前集合中元素的个数也从10变成了11。

10°集合的第二次扩容结束。

接着,我们跳出内层add方法,一直跳到演示类的代码中,如下GIF所示 :

11°将集合中的元素添加到15个。(第二个临界点)

之后的第12到第15个元素的添加,与第十一个元素的添加大同小异,只不过在内层的add方法中,我们不再进入grow方法(即数组不需要扩容),而是直接将元素添加到elementData数组中。因此,接下来4个元素的添加,我们一笔带过(有兴趣的小伙伴儿可以自己下去Debug一下),如下GIF演示图 :

12°集合的第三次扩容开始。

第二次扩容结束后,底层的elementData数组由10扩容到了15。而经过我们的一通操作过后,elementData数组又满了。现在我们想向集合中添加第16个元素,就要进行集合的第三次扩容。从第二次扩容开始,之后的每次扩容在底层都与第二次扩容原理一样,并且每次都扩容到当前集合容量的1.5倍。

好的,同样地,我们先跳入外层add方法。如下 :

接着,再跳入内层add方法。如下 :

可以看到,if条件语句的判断中,又提示" = true"了,因此,我们还要再跳入外层grow方法中。如下 :

显式当前集合中元素的个数为size = 15。继续跳入内层grow方法 :

内层for循环中if语句的判断条件也为true。继续跳入newLength方法 :

可以看到,新数组的预设长度是22,恰好等于15 + 15 / 2 = 22,也再次印证我们之前的结论——扩容到1.5倍。

13°集合的第三次扩容结束。

好滴,接着我们再逐层返回,一直跳回到内层add方法中,如下GIF图所示 :

继续执行内层add方法中的代码,如下图所示 :

可以看到,elementData数组的长度成功地由15扩容到了22。

接下来,我们逐层返回,一直返回到演示类中,如下GIF图所示 :

🆗,无参构造的分步骤Debug演示,就到这里结束了。相信只要你把这一套流程吃透,自己可以Debug下来。那么其他情况下的扩容流程你也可以轻松举一反三了。

3.带参构造——分步骤Debug(详细阐释)

0°前言 :

如果利用带参构造来初始化ArrayList对象,那么它底层的扩容机制,其实与无参构造初始化ArrayList对象时大同小异。唯一不同的一点在于,使用带参构造初始化ArrayList对象,底层的elementData数组在一开始不会置空,而是将其初始化为调用带参构造时中实参指定的长度。之后的扩容流程与空参构造初始化对象时无异。因此,up这里就不会像之前空参构造时演示得那么细了。

up以ArrayList_Demo2为例,代码如下 : (13行和22行设置断点)

package csdn.knowledge.api_tools.gather.list;import java.util.ArrayList;/*** @author : Cyan_RA9* @version : 21.0*/
public class ArrayList_Demo2 {public static void main(String[] args) {//演示 : 测试通过带参构造初始化ArrayList集合时,其底层的数组扩容机制。//1.创建ArrayList集合对象ArrayList arrayList = new ArrayList(4);System.out.println("刚创建的集合 = " + arrayList);//2.向集合中添加元素(先来4个)for (int i = 0; i < 4; i++) {arrayList.add(i);       //此处涉及到了自动装箱。}//3.再次向集合中添加元素。(扩容:4 ——> 6)arrayList.add("这是第五个元素捏");arrayList.add("这是第六个元素捏");System.out.println("添加六个元素后,集合 = " + arrayList);//4.再次向集合中添加元素。(扩容:6 ——> 9)arrayList.add("这是第七个元素捏");System.out.println("添加七个元素后,集合 = " + arrayList);}
}

1°开始Debug。

如下图所示,进入Debug界面:

2°集合的第一次扩容(初始化)。

接着,我们跳入ArrayList的带参构造,如下图所示 :

可以看到,elementData数组被初始化为了长度为4的数组,4正是我们调用带参构造时指定的长度。其实,可以看到ArrayList类的该带参构造是一个if-else if-else的符合条件语句。因为我们指定的长度4 > 0,所以它直接进入if控制的语句中,即将一个长度为4的新数组赋值给了elementData数组(其实就是改变了elementData引用的指向)。如果带参构造传入的实参为0,它就会当作空参构造来执行。如果 < 0,就会抛出一个异常对象。

接着,我们跳出带参构造。如下图所示 :

注意,我们可以看到底层的elementData数组已经被初始化为4的长度。

3°向集合中添加第一个元素。

继续Debug,跳入for循环的add方法。同之前一样,这里我们要添加int类型的数据进入几何,底层会有装箱的过程,我们直接跳出即可,如下GIF图所示 :

接着,我们再次跳入add方法中,如下图所示 :

可以看到,这不就是上面演示的外层add方法吗?是的,也就是说,从elementData数组的初始化后,带参构造集合在扩容时,底层所有的操作都同无参构造一致。

因此,下面的演示up就多以GIF图来呈现了。其实只要上面那个无参的你会了,这个过一遍就🆗了。

接下来,我们完成集合中第一个元素的添加,如下GIF图所示 :

4°将集合中的元素添加到4个。(第一个临界点)

由于底层的elementData数组初始化时长度为4。因此前四个元素的添加均无二致。我们一笔带过就好,如下GIF图演示 :

5°集合的第二次扩容。

当前集合的元素已达4个,要想向集合中添加第五个元素,需要再次进行扩容。扩容机制同上面的演示一样,这里不再赘述。如下GIF图演示 :

6°将集合中的元素添加到6个。(第二个临界点)

如下GIF图所示 :

7°集合的第三次扩容。

经过两次扩容,集合的容量从0 ——> 4 ——> 6,那么第三次扩容,就应该是6的1.5倍 = 9了。我们来看看是不是,如下GIF图所示 :

可以看到,第三次扩容后,elementData数组的长度确实从6扩到了9。如下图所示 :

四、总结

🆗,以上就是我们ArrayLIst源码分析的全部内容了。其实,如果你看过JDK8.0的源码,不难发现两个在底层的实现上有着异曲同工之妙。当然,就up个人主观来看,两者的差异主要体现在了方法的形参和方法的位置上。话说回来,非常建议大家跟着up一起Debug一下,看源码,通过Debug去了解源码的结构,有助于提升大家对于源码的阅读力。感谢阅读!

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...