java Vector 源码分析(深度讲解)
创始人
2025-05-28 14:42:20
  • Vector类的底层实现

  • Vector类 VS ArrayList类

  • Vector类源码解读

  • 无参构造——分步骤详解

  • 有参构造——分步骤演示


一、前言

1.大家好,本篇博文是对单列集合List的实现类之一——Vector类的内容分享。up会利用断点调试(Debug)来一步一步地给大家剖析Vector底层的扩容机制到底是如何实现的。
2.其实,up前不久刚刚出过一篇对于ArrayList类的源码解读的博文,讲得详细,建议大家在看这篇博文之前优先去看一下up对ArrayList类的源码分析。因为二者同属List接口的实现类,而且底层的一些实现细节具有异曲同工之妙。所以,这篇肯定比不上ArrayList那篇详尽。
3.注意 : ①解读源码需要扎实的基础,比较适合希望深究的同学; ②不要眼高手低,看会了不代表你会了,自己能全程Debug下来才算有收获; ③点击文章的侧边栏目录或者前面的目录可以进行跳转。 ④本篇博文对Vector源码的解读基于JDK17.0的版本,虽然不是主流的JDK8.0,但是经过对比不难发现其底层原理大同小异,所以,就算你用的不是高版本的JDK,跟着文章中up的演示一起过一遍,也会对你有一定的提示。良工不示人以朴。 感谢阅读!

二、Vector类底层实现

1.同ArrayList一样,Vector底层也是由一个Object类型的数组来实现的。如下图所示 :

2.当我们使用空参构造来创建Vector类对象时,则elementData数组的初始容量默认为10,如需再次扩容,则将elementData数组的当前容量扩容为2倍。

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

4.Vector是线程同步的,即线程安全的,这是因为Vector类的操作方法带有synchronized修饰符。因此,在开发中需要线程同步安全时,考虑使用Vector类;如果是单线程情况下,建议优先使用ArrayList类,因为它的效率更高。


三、ArrayList类 与 Vector类的比较

与ArrayList类相比,Vector类的不同之处主要有两个:首先Vector类是线程同步的,而ArrayList类线程不同步。其次,Vector类的扩容机制不再是0 ——> 10 ——> 1.5倍 ——> 1.5倍 ——> .....;而是一开始就令底层的elementData数组指向了默认长度为10的数组,之后每次扩容都是按2倍来扩的。

ArrayList类和Vector类的更多对比,如下图所示 :


四、Vector类源码解读(断点调试)

1.无参构造——分步骤详解 :

〇准备工作。

up以Vector_Demo类为演示类,代码如下 : (20行,24行下了断点)

package csdn.knowledge.api_tools.gather.list;import java.util.Vector;/*** @author : Cyan_RA9* @version : 21.0*/
public class Vector_Demo {public static void main(String[] args) {//演示 : Vector类源码分析————Vector集合的底层扩容机制//1.创建Vector集合对象Vector vector = new Vector();System.out.println("当前集合 = " + vector);//2.向Vector集合中添加元素到10个。()--- 第一次扩容for (int i = 0; i < 10; i++) {vector.add(i);}System.out.println("添加十个元素后,当前集合 = " + vector);//3.继续向集合中添加元素。(10 ——> 20)--- 第二次扩容for (int i = 0; i < 10; i++) {vector.add(10 + i);}System.out.println("添加二十个元素后,当前集合 = " + vector);//4.继续向集合中添加元素。(20 ——> 40)--- 第三次扩容vector.add("这是集合第二十一个元素捏");System.out.println("添加二十一个元素后,当前集合 = " + vector);}
}

①开始Debug。

如下GIF图所示,我们进入上面演示类的Debug界面 :

②跳入Vector无参构造。

好的,接下来我们跳入Vector类无参构造器中看看情况如何,如下图所示 :

可以看到,Vector类的无参构造器默认调用了本类的一个带参构造器。注意看形参列表,IDEA给出了提示"initialCapacity",见名知意,传入的实参将决定Vector集合的初始容量,即底层elementData的初始长度。但是,这里的"initialCapacity"是一个固定值"10"。所以,这也就验证了我们上文提到的——当我们使用空参构造来创建Vector类对象时,则elementData数组的初始容量默认为10。我们继续往下追,看看这个被调用的带参构造器长什么样,如下图所示 :

好家伙,Vector这包皮属实有点长了,该有参构造里面调用了另外一个有参构造。注意此处的形参列表出现了变化,除了elementData数组的初始长度10,又多传入了一个IDEA提示为"capacityIncrement"的实参,同样,见名知意,这里传入的"0"指的是"Vector集合的容量增量为0",这也很好解释,我们还没向集合中添加元素呢,要什么增量捏。我们也可以通过Ctrl + b/B快捷键来查看一下"capacityIncrement"的源码说明,如下 :

简单翻译一下,源码中给出的说明是:当集合中要添加的元素的总个数大于底层elementData数组的总长度时,集合会自动扩容,"capacityIncrement"变量便保存了自动扩容的增量。并且该变量可作为一个判断标准,当capacityIncrement ≤ 0时,集合的容量会自动扩容到原来的两倍。

好的,我们先通过Ctrl + Alt + ← 快捷键回到刚刚的有参构造,然后我们刨根问底,再给它往下追一步,如下图所示 :

首先第一句super();我们不用管,那只是调用了一下Vector的父类AbstractList类的一个空参构造,里面啥也没。if条件语句在这里的作用是:如果预设的集合初始容量小于0,就抛出一个异常对象。

回到正题,注意看up用彩色线条标注出的部分,可以明显地看到,elementData数组被初始化为了一个长度为10的数组。这也是与ArrayList类无参构造一个较大的区别——我们知道,数组属于引用类型,引用类型的默认值为null,所以不管是ArrayList类还是Vector类,只要创建它们的对象,底层的elementData数组一开始均为null。但是空参构造初始化ArrayList对象时,它会先将数组置空,这里的置空指的是"将一个空数组{} 赋给elementData",然后等集合第一次添加元素时再进行扩容;而Vector类则是没有了"置空"这一步骤,直接就将elementData数组由null初始化为了一个长度为10的数组,等你什么时候加满了再扩容。

③elementData数组初始化完成。

好的,接下来我们逐层跳出构造器,一直跳回我们的演示类中,如下GIF图所示 :

这时,我们已经可以看到elementData数组被初始化成功了,如下图所示 :

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

好的,接下来我们继续Debug,逐行执行进入第一个for循环后,我们准备跳入add方法。但是,由于我们第一个for循环添加的十个元素是0~9,所以底层肯定会进行自动装箱。这里我们不管它,直接跳出来,准备重新跳入add方法,如下GIF图所示 :

接下来,我们重新跳入外层add方法,如下图所示 :

可以看到,与ArrayList类中的外层add方法模样大同小异。明显有两点不同的是——①Vector类的外层add方法用了synchronized关键字修饰,表示线程同步,这个我们在上文Vector类的底层实现中提到过;②内层add方法的形参列表中,用了"elementCount"变量来表示当前集合中元素的个数,这倒无关要紧,与ArrayList内层add方法中的size变量同理,换汤不换药。modCount我们在ArrayList类源码分析中说过了,也不影响我们对Vector源码的解读,因此这里不再赘述。

还有注意,外层add方法的形参列表"E e",表示当前将要向集合中添加的元素,因此这里e = 0。接下来,我们继续跳入内层add方法中一探究竟,如下图所示 :

诶,看过up上一篇ArrayList类源码解读的小伙伴儿就知道,像,真™像啊。

首先一个if条件语句的判断,如果 "当前集合中已有元素的个数等于底层elementData数组的长度" 判断成立,就准备进入grow方法对数组进行扩容。当然,还是比较容易理解,如果目前集合中已有元素的个数和elementData数组的长度相等,那意思不就是满了么?我们就要对它进行扩容。

当然,由于Vector类的无参构造在一开始就把elementData数组初始化为长度 = 10了,因此这里判断为false,我们直接往进加就完了!最后再令elementCount + 1,表示当前集合中元素的个数由0变成1了。

如下GIF图所示,我们继续Debug,并逐层跳回,直到演示类 :

⑤继续添加元素,直到满10个。

好的,后面的1~9这九个元素的添加与第一个元素0的添加过程一致。我们一笔带过,如下GIF图所示 :

可以看到,第一个for循环的十个元素已经全部添加到了集合中,如下图所示 :

⑥第一次扩容开始。(10 ——> 20)

由于集合对象在初始化时,底层的elementData数组默认长度为10,而现在我们已经添加了10个元素了。如果想继续添加第十一个元素到集合中,就必须对elementData数组进行扩容了。

好的,我们继续Debug,进入第二个for循环,并准备跳入外层add方法。第二个for循环中,我们要像集合中添加10~19这十个元素,因此,第一次跳入外层add方法,会跳入自动装箱的过程,我们不管它,直接跳出,并准备重新跳入外层add方法,如下GIF 图所示 :

好的,我们跳入外层add方法,如下图所示 :

可以看到,我们正要添加的第十一个元素e = 10。接着,继续跳入内层add方法,如下图所示 :

好的,现在是内层add方法中,if条件语句的判断显然满足(10 == 10),因此,我们准备进入grow方法对elementData数组进行扩容了。注意,这里"elementData = grow(); "语句,显然是改变了elementData的指向,即grow方法最终要返回一个Object类型的数组。

跳入外层grow方法,如下图所示 :

嗯,还是和ArrayList一样,包皮是一层包一层。注意看形参,"minCapacity",见名知意,它是指"要把当前元素添加到elementData数组中,所需数组的最小容量",原本数组中已经有10个元素了,现在要添加第十一个,当然所需的最小容量就是10 + 1 = 11了,不然你怎么放进去。

进入跳入内层grow方法中,如下图所示 :

注意了,正片现在开始了!我们先看整体 :

内层grow方法中,首先是将当前elementData数组的长度赋值给了oldCapacity变量,有什么用呢,我们暂时不管。继续往下看,下面是一条对newCapacity变量的赋值语句,见名知意,newCapacity变量与我们最终要返回的新数组的长度绝对有着密切关系。

果不其然,最下面的return语句返回一个新的elementData数组(注意,赋值表达式的值等于赋值后变量的值),并且新的数组由copyOf函数生成,这函数我们已经说过很多次了,直接告诉你它的功能——将原数组中指定长度的内容拷贝到新数组中,并且,若指定的长度大于原数组长度,则多出来的部分以默认值填充,最终返回的是新数组。

这下,内层grow函数的整体情况我们算是了解了,接着我们来看看newCapacity变量的赋值语句中,Vector到底使用了怎样的扩容机制呢?

为newCapacity变量赋值的是newLength方法,我们先不急着跳入,先来看看它的形参列表 : ①oldCapacity——原数组的长度;②minGrowth = minCapacity - oldCapacity,所需数组的最小长度减去数组当前的长度,即数组的最小增长量;③一个三目运算符的表达式。我们来单独说一说这个三目运算符的表达式,如下图所示 :

定睛一看,诶?"capacityIncrement"变量好像在哪儿见过?自信点儿!TMD上面我们还看过它的源码呢。(忘记的小伙伴儿可以通过侧边栏目前跳转到"②跳入Vector无参构造。"中去回顾一下。)

这个三目运算符的意思是,如果这个变量 > 0,就返回这个变量的值;如果这个变量 ≤ 0,就返回数组当前的长度。好的,我们可以将鼠标悬停上去,看看capacityIncrement变量的值到底是多少,如下图所示 :

哟,等于0呐,那好说了。三目运算符最终返回的就是当前数组的长度。所以,此处newLength方法最终传入的实参就是:①当前数组的长度;②数组的最小增长量;③当前数组的长度。这时候可能会有p小将(Personable小将,指分度翩翩的人)出来bb问了:Y的,传入两个当前数组的长度是干什么玩意儿?

p小将你别急,你不跳入newLength方法,你咋知道它怎么实现的?因此,下一步我们跳入newLength方法,如下图所示 :

注意看,"prefLength",也是老面孔了,预设长度。为它赋值的是当前数组长度 + max函数的返回值。max函数中,minGrowth = 1,prefGrowth = 10(即第三个形参,前面三目运算表达式的返回值)。所以,prefLength = 10 + 10 = 20。这也印证了我们上文中提到过的——当我们使用空参构造来创建Vector类对象时,则elementData数组的初始容量默认为10,如需再次扩容,则将elementData数组的当前容量扩容为2倍。至于后面的if判断语句,它是判断prefLength有没有超出常规范围,哎,咱就一个测试类水水博文,咋可能超范围,不管他。

这下水落石出了,newLength方法最后要返回的是新数组的长度,而新数组最后要在内层grow方法中,通过copyOf方法达到"更改elementData引用的指向"的目的,使它指向这个新数组。

⑦第一次扩容结束。

ok,明白了Vector底层的扩容机制后,我们逐层返回,直到内层add方法,如下GIF图 :

在内层add方法中,我们可以看到elementData数组中已经成功添加了第十一个元素,并且其容量已经显示为了20,如下图所示 :

然后,我们继续往回跳,一直跳回测试类中,如下图所示 :

⑧第二次扩容开始。(20 ——> 40)

第十二到第二十个元素的添加没啥好演示的,我们直接一笔代过,并且直接在添加第二十一个元素的地方下个断点,跳到那里,准备进行第二次扩容。如下GIF图所示 :

⑨第二次扩容结束。

因为Vector类的扩容机制本身就与ArrayList类的扩容机制大同小异,而且我们上面已经完整的演示过一次扩容的流程了。所以,第二次扩容的过程up就不再一一详细地展开了,我们直接以一个GIF图来展示第二次扩容的Debug全流程。如下GIF图演示 :

🆗,以上就是我们空参构造初始化Vector类对象,Vector底层扩容机制的全流程分析了。下面我们再来看看带参构造的情况。

2.有参构造——分步骤演示 :

Δ前言 :

鉴于有参构造与无参构造的底层实现基本相同。只是在第一步初始化底层elementData数组时略有出入,这一点我们在上面"ArrayList VS Vector"的表格中已经给出,并且在上文"Vector的底层实现"中也提到过。所以,有参构造的演示,up不会再像无参构造那样一步一步详细走了。绝对不是因为我懒!

好吧,必须承认,写这种解读源码文章还是有点小累的。但是也确实没必要都那么来一遍,一个是你也看不下去,一个是给大家自己下去练习的机会。好了,不废话了,我们开始吧。

〇准备工作。

up以Vector_Demo2类为演示类,代码如下 : (13行,21行设置断点)

package csdn.knowledge.api_tools.gather.list;import java.util.Vector;/*** @author : Cyan_RA9* @version : 21.0*/
public class Vector_Demo2 {public static void main(String[] args) {//演示 : Vector有参构造初始化,Vector的底层扩容机制。//1.创建Vector类对象,并指定初始容量。Vector vector = new Vector(5);//2.向集合中添加元素,直到满5个。for (int i = 0; i < 5; i++) {vector.add(i);}//3.向集合中添加第六个元素。(第一次扩容)5 ——> 10vector.add("第六个元素捏");//4.继续向集合中添加元素,直到满十个。vector.add("第七个元素捏");vector.add("第八个元素捏");vector.add("第九个元素捏");vector.add("第十个元素捏");//5.向集合中添加低十一个元素。(第二次扩容)10 ——> 20vector.add("第十一个元素捏");}
}

①初始化elementData数组。

直接开始吧。以下GIF图演示——①跳入有参构造;②跳出有参构造GIF图如下 :

可以看到,我们通过有参构造将elementData数组初始化为了一个长度为5的数组,如下图所示 :

②第一次扩容开始。

继续,以下GIF图演示——①添加第一个元素;②添加元素直到满5个;如下GIF图演示 :

由于我们初始化集合时,只指定了5的大小,因此这时候,要想添加第六个元素,就需要对底层的elementData数组进行扩容操作。

我们继续,以下GIF演示图演示——①跳入外层add方法;②跳入内层add方法;③跳入外层grow方法;④跳入内层grow方法;⑤跳入newLength方法。如下GIF图所示 :

③第一次扩容结束。

接着,我们逐层跳出,直到演示类中。

可以看到,集合已经完成了第一次扩容,容量显示为了10,并且第六个元素也成功添加到了集合中。如下图所示 :

④第二次扩容开始。

接着,我们继续向集合中添加元素,直到满十个。如下图所示 :

继续,向集合中添加第十一个元素需要进行第二次扩容。以下GIF图演示——①跳入外层add方法;②跳入内层add方法;③跳入外层grow方法;④跳入内层grow方法;⑤跳入newLength方法。如下GIF图所示 :

⑤第二次扩容结束。

接着,我们逐层跳出,直到演示类中。

可以看到,集合已经完成了第二次扩容,容量显示为了20,并且第十一个元素也成功添加到了集合中。如下图所示 :


五、总结

🆗,以上就是我们Vector源码解读的全部内容了。同样的,JDK17.0版本下Vector底层的扩容机制与JDK8.0版本的有异曲同工之妙,但是up总觉得高版本的看着更顺眼,可能是因为用习惯了的原因😂。希望大家下去以后多多练习,最起码跟着up过一遍。阅读源码的能力需要慢慢培养,我们今天就到这里,感谢阅读!

相关内容

热门资讯

vi 编辑命令 命令Summary: movement keysHere's a recap of the move...
(十六)qt creator中... 文章目录1.Qt使用qmake的工程添加OpenCV库2.Debug时常见问题 欢迎访问个人网络日...
X99主板2011-3接口E5... 最近发现精粤出了好多X99的主板,博主从intel 网站整理了X99主板2011-3接...
工信部认可老年代步车目录,十大... 本篇文章极速百科给大家谈谈工信部认可老年代步车目录,十大名牌老年代步电动四轮...,以及国家工信部公...
XNV为什么不挂本田logo?... 今天给各位分享XNV为什么不挂本田logo?东风本田XNV优缺点有哪些的知识,其中也会对东风本田xn...
细节有改动试驾2013款宝骏6... 本篇文章极速百科给大家谈谈细节有改动试驾2013款宝骏630自动型,以及2013款宝骏630舒适型对...
jetson-xavier-n... 1 sdkmanager刷写系统sdkmanager启动失败使用sdkmanager刷机时可能会遇到...
车轮为什么是圆形的(车轮为什么... 今天给各位分享车轮为什么是圆形的的知识,其中也会对车轮为什么是圆形的车轴应该装在什么位置进行解释,如...
车载以太网 - 测试用例设计 ... 关于Alive check和Mode Check相关的介绍前面的文章已经有了比较清晰的介绍,测试用例...
【Linux】信号常见概念 文章目录信号入门生活中的信号技术应用角度的信号signal函数注意事项信号的概念信号的产生信号的记录...
英菲尼迪Q50英菲尼迪Q50最... 今天给各位分享英菲尼迪Q50英菲尼迪Q50最新报价-图片-参数的知识,其中也会对英菲尼迪q50官方报...
如何看中国超燃冲压发动机获得突... 今天给各位分享如何看中国超燃冲压发动机获得突破,地面试验实现连续...的知识,其中也会对中国超燃冲压...
兰博基尼雷文顿多少钱兰博基尼雷... 本篇文章极速百科给大家谈谈兰博基尼雷文顿多少钱兰博基尼雷文顿贵吗?,以及兰博基尼雷文顿跑车图片对应的...
苦恼的反义词(苦恼的反义词最佳... 今天给各位分享苦恼的反义词的知识,其中也会对苦恼的反义词最佳答案进行解释,如果能碰巧解决你现在面临的...
命令行工具检索命令find 和... grep检索文件包含的内容的命令使用 grep 命令可以检索文件包含的内容,例如&#x...
第一章:职场入门:程序员如何开... 作为一名Java程序员,我们深知在当今激烈的市场竞争中,如何开始职业生涯是至关重要的。本章将从多个方...
C语言:文件的读写(fputc... 近段时间,在重新学习一下C语言程序设计,学习到了文件读写这一章节,觉得这方面的知识较复杂,于是把其中...
清华大学土木工程系包含哪些专业... 今天给各位分享清华大学土木工程系包含哪些专业的知识,其中也会对清华大学土木工程系包含哪些专业课程进行...
秦国卫鞅怎么死的(卫鞅最后有没... 今天给各位分享秦国卫鞅怎么死的的知识,其中也会对卫鞅最后有没有娶秦国公主进行解释,如果能碰巧解决你现...
美利达车架号(美利达车架号能查... 今天给各位分享美利达车架号的知识,其中也会对美利达车架号能查出什么信息进行解释,如果能碰巧解决你现在...
马杀鸡什么意思(日语马杀鸡什么... 本篇文章极速百科给大家谈谈马杀鸡什么意思,以及日语马杀鸡什么意思对应的知识点,希望对各位有所帮助,不...
一次 JVM 类加载异常 文章目录1. JVM 类加载异常1. 出现问题2. 解决过程1. JDK 7 版本过老2. JDK ...
Button(按钮)与Imag... 今天给大家介绍的Android基本控件中的两个按钮控件,Button普通按钮和ImageButton...
vue子组件无法根据prop属... 问题描述 在vue中,有一个父组件和一个子组件,在父组件里有一个变量&#...
雪佛兰SPARK是什么车?SP... 今天给各位分享雪佛兰SPARK是什么车?SPARK现在还有卖吗的知识,其中也会对2020雪佛兰spa...
全世界最贵的跑车(全世界最贵的... 今天给各位分享全世界最贵的跑车的知识,其中也会对全世界最贵的跑车是啥进行解释,如果能碰巧解决你现在面...
e哥什么意思(e哥是谁啊) e... 今天给各位分享e哥什么意思的知识,其中也会对e哥是谁啊进行解释,如果能碰巧解决你现在面临的问题,别忘...
推荐国内十大品牌润滑油(国内知... 今天给各位分享推荐国内十大品牌润滑油的知识,其中也会对国内知名品牌润滑油进行解释,如果能碰巧解决你现...
前端性能优化之HTTP缓存 前端缓存 前端缓存可分为两大类:HTTP 缓存和浏览器缓存。 我们今天重点是 HTTP...
Linux 端口号占用如何处理 在Linux中,可以使用以下命令来查看端口号的占用情况: sudo ne...