java Vector 源码分析(深度讲解)

目录

一、前言

二、Vector类简介

三、Vector类底层实现

四、ArrayList类 与 Vector类的比较

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

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

            〇准备工作。

            ①开始Debug。

            ②跳入Vector无参构造。

            ③elementData数组初始化完成。

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

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

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

            ⑦第一次扩容结束。

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

            ⑨第二次扩容结束。

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

            Δ前言 :

            〇准备工作。

            ①初始化elementData数组。( = 5)

            ②第一次扩容开始。(5 --> 10)

           ③第一次扩容结束。

            ④第二次扩容开始。(10 --> 20)

            ⑤第二次扩容结束。

六、总结


一、前言

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

        PS : 以上前言写于2023.3.15初发布。

        2023.10.28,今天把这篇文章做了点完善,又看完一遍的感受就是:写得太繁琐了,毫不夸张地讲就是又臭又长。这里诚恳地建议大家——可以直接去看up的ArrayList类源码分析,看过后可以直接看Vector类源码解读中的"跳入Vector无参构造"和“第一次扩容开始”的相关部分。haha,总之,感谢大家阅读。


二、Vector类简介

                Vector类是单列集合List接口的一个实现类。与ArrayList类似,Vector也实现了一个可以动态修改的数组,两者最本质的区别在于——Vector类是支持线程同步的,因此它线程安全,支持多线程;而ArrayList是线程不同步的,线程不安全。
                Vector类属于java.base模块,java.util包下,如下图所示 :

                我们再来看看Vector的类图,如下所示 :

                Vector的类图与ArrayList的类图一模一样,这也间接印证了它们相似的特点。


三、Vector类底层实现

                1.同ArrayList一样,Vector底层也是由一个Object类型的数组来实现的 (注意Vector维护的elementData数组没有用transient关键字修饰)。如下图所示 :

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

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

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


四、ArrayList类 与 Vector类的比较

        与ArrayList类相比,Vector类的不同之处主要有两个——
        1.首先Vector类是线程同步的,而ArrayList类线程不同步。
        2.其次, 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方法如下图所示 :

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

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

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

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

            ⑨第二次扩容结束。

                第一次扩容结束后,elementData数组的长度由10扩到了20。而我们通过第二个for循环,已经将集合中的元素添加到了20个。所以,要想向集合中添加第二十一个元素,就需要对elementData数组进行第二次扩容了。

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

                可以看到,经过第二次扩容,elementData数组的长度已经由20扩到了40如下图所示 :

                🆗,以上就是我们空参构造初始化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 ——> 10
        vector.add("第六个元素捏");

        //4.继续向集合中添加元素,直到满十个。
        vector.add("第七个元素捏");
        vector.add("第八个元素捏");
        vector.add("第九个元素捏");
        vector.add("第十个元素捏");

        //5.向集合中添加低十一个元素。(第二次扩容)10 ——> 20
        vector.add("第十一个元素捏");
    }
}

            ①初始化elementData数组。( = 5)

                直接开始吧。我们先跳入Vector类的有参构造,如下图所示 :

                可以看到,这个有参构造调用了——与无参构造内部调用的有参构造相同的构造器,但是区别在于,此处我们的"initialCapacity"初始容量不再是固定数值10了,而是我们传入的实参5

                继续跳入,可以看到对elementData数组初始化的语句,如下图所示 :

                接着,我们逐层跳出,跳回演示类中。可以看到,我们通过有参构造将elementData数组初始化为了一个长度为5的数组,如下图所示 :

            ②第一次扩容开始。(5 --> 10)

                继续,我们向Vector集合中添加5个元素。因为这里添加元素的步骤同上文中的无参构造情形完全一致,所以我们下个断点,一步带过就好,如下GIF图所示 :

                由于我们初始化集合时,只指定了5的大小,因此这时候,要想添加第六个元素,就需要对底层的elementData数组进行扩容操作。当然,这里的扩容流程与上文中无参构造的情形完全一致,也没必要再一步一步演示了。up给大家过一遍流程,加深一下印象就好。

                第一次扩容流程,如下GIF图所示 :

           ③第一次扩容结束。

                接着,我们逐层跳出,直到演示类中。
                可以看到,集合已经完成了第一次扩容,容量显示为了10,并且第六个元素也成功添加到了集合中如下图所示 :

            ④第二次扩容开始。(10 --> 20)

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

                向集合中添加第十一个元素需要进行第二次扩容。第二次扩容流程,如下GIF图所示 :

                通过以上GIF图我们可以看到elementData数组的长度已经由10扩到了20。

            ⑤第二次扩容结束。

                接着,我们逐层跳出,直到演示类中。
                可以看到,集合已经完成了第二次扩容,容量显示为了20,并且第十一个元素也成功添加到了集合中如下图所示 :


六、总结

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

        System.out.println("END------------------------------------------------------------------");

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan_RA9

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值