外贸网站定制,郑州网站建设丶汉狮网络,北京营销型网站建设费用,网站建设适合的企业ArrayList类的底层实现ArrayList类的断点调试空参构造的分步骤演示#xff08;重要#xff09;带参构造的分步骤演示一、前言大家好#xff0c;本篇博文是对单列集合List的实现类ArrayList的内容补充。之前在List集合的万字详解篇#xff0c;我们只是拿ArrayList演示了List…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~9for (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 --- 22arrayList.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底层数组为空所以目前数组的大小 00 0满足判断条件。所以肯定要进入这个grow方法。grow方法见名知意就是对数组进行扩容的方法。elementData grow();显然grow方法最终是返回一个Object类型的数组这样才能赋值给elementData数组其实就是更改了elementData的引用。我们也是一路高歌继续跳入grow方法中看看是咋回事。如下图所示 : 果然grow方法返回了一个Object类型的数组。不过比较操蛋的是grow方法与前面的add方法类似都™长了层包皮没关系我们继续追进去看看。注意 : 既然外层的grow方法是返回Object类型的数组说明return后面的grow方法肯定要是返回一个Object类型的数组。不过内层的grow方法传入了一个形参是size 1size表示当前集合中元素的个数第一个元素还没加进去所以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数组的长度 1010 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 —— 6arrayList.add(这是第五个元素捏);arrayList.add(这是第六个元素捏);System.out.println(添加六个元素后集合 arrayList);//4.再次向集合中添加元素。扩容6 —— 9arrayList.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源码分析的全部内容了。其实如果你看过JDK.的源码不难发现两个在底层的实现上有着异曲同工之妙。当然就up个人主观来看两者的差异主要体现在了方法的形参和方法的位置上。话说回来非常建议大家跟着up一起Debug一下看源码通过Debug去了解源码的结构有助于提升大家对于源码的阅读力。感谢阅读