1.2 List集合实现——Vector
java.util.Vector集合是从Java早期版本(从JDK 1.0开始)就开始提供的一种List集合,其主要的继承体系如图1-8所示。
图1-8
根据图1-8可知,Vector集合是支持随机访问的,该特性在1.1节中已经进行了讲解,这里不再赘述。Vector集合的工作特性除了支持数据对象的随机访问,还有集合的大小可变、保证线程安全的运行环境等。
下面对Vector集合中的典型操作进行详细介绍(注意源码版本为JDK 9+,直到JDK 14官方都未对该源码进行调整)。首先介绍存在于Vector集合(及其上级AbstractList类)中的重要变量信息,源码如下。
根据对以上3个重要变量的描述可知,Vector集合的基本结构包括一个数组、一个指向当前数组的可验证边界、一个数组扩容的参考值,如图1-9所示。
图1-9
在Vector集合中,elementCount变量非常重要,它在Vector集合进行写性质操作时会发生变化。elementCount变量的值可以小于elementData数组的容量值(使用capacity()方法可获取当前数组的容量值),也可以和elementData数组的容量值相等。当elementData数组中的每个索引位上都添加了数据对象时,可以将这些数据对象设置为null。
1.2.1 Vector集合的扩容操作
1. 什么时候扩容
Vector集合需要进行扩容操作的情况有多种。Vector集合在初始化时会进行扩容操作,它的elementData数组会进行初始化;在Vector集合中,当数据对象数量elementCount的值大于elementData数组的最大容量capacity的值时,也会进行扩容操作,这时,elementData数组中的数据对象会被复制到另一个更大的数组中,并且elementData数组变量的引用会重新指向后者;当Vector集合的调用者明确要求重新确认集合容量时,也可能会进行扩容操作。
1)在初始化Vector集合时,扩容过程的详细源码如下。
上述3个构造方法的执行过程和关联关系一目了然,不需要再做过多说明。根据上述源码片段可知,如果没有在Vector集合初始化时指定集合的初始化容量(initialCapacity),则会将初始化容量值设置为10;如果没有在Vector集合初始化时指定扩容增量(capacityIncrement),则会将扩容增量值设置为0。如果扩容增量值被设置成了0,那么在随后进行的每次扩容操作中,elementData数组扩容后的容量都会变为扩容前容量的2倍,即10->20->40->80……以此类推。
2)在当前Vector集合中的数据对象总量超出数组容量上限时,会进行扩容操作。
以Vector集合中的add(E)方法为例,源码片段如下。
add()方法的操作过程分为以下两种情况。
• 当集合中对象数据数量elementCount的值小于当前elementData数组容量的值时,不必对elementData数组进行扩容操作,直接在elementData数组中的elementCount号索引位上添加新的数据对象即可。
• 当集合中数据对象数量elementCount的值大于或等于elementData数组容量的值时,需要先对elementData数组进行扩容操作,再在elementCount号索引位上添加新的数据对象。
3)当调用者明确要求重新确认Vector集合容量时,也可能会进行扩容操作。例如,Vector集合中的setSize(int)方法允许调用者重新为Vector集合设置一个容量值,如果这个容量值大于当前Vector集合的容量值,则会进行扩容操作;如果新的容量值小于当前容量值,则会将多余的数据对象丢弃,源码片段如下。
注意:在特定的场景中,可以减小Vector集合的数组容量(缩容)。可查看Vector集合中的trimToSize() 方法。
2. 详细的扩容过程
根据前面的源码可知,Vector集合进行扩容操作的主要方法是grow(int)方法,所以我们主要对这个方法进行分析(在JDK 14中,该方法已经简化,更容易理解),源码如下。
根据上述源码可知,如果当前Vector集合没有在实例化时指定增量capacityIncrement的值,那么在一般情况下,每次扩容增加的容量都是当前容量的1倍;如果当前Vector集合在实例化时指定了增量capacityIncrement的值,那么在一般情况下,会按照指定的增量capacityIncrement的值进行扩容操作。
下面重点讲解一下Arrays.copyOf()方法和ArraysSupport.newLength()方法。
1)Arrays.copyOf(T[] original, int newLength)方法。
该方法是一个工具性质的方法,主要用于将原始数组(original)复制为一个新的数组,后者的长度为指定的新长度(newLength)。
按照这样的描述,对于指定的新长度(newLength),会出现以下两种情况。
• 指定的新长度(newLength)小于原始数组(original)的长度,那么原始数组(original)无法复制的部分会被抛弃。
• 指定的新长度(newLength)大于或等于原始数组(original)的长度,那么原始数组(original)中的所有数据对象(的引用)会按照原来的索引位被依次复制到新的数组中,新数组中多出来的空余部分会被填充为null,如图1-10所示。
图1-10
图1-10中不包括新长度(newLength)无效的情况。例如,当newLength的值为负数时,Arrays.copyOf(T[] original, int newLength)方法会抛出java.lang.NegativeArraySizeException异常。有的读者会问,当newLength的值为0时,会出现什么情况?这种情况满足以上描述的第一种情况——没有任何数据对象可以填充,所以会输出一个空数组。
此外,图1-10中也不包括对Java基础类型数组(int[]、long[]、float[]等)进行复制的场景。在这些基础类型数组的复制过程中,新数组中多余的位置上会填充这个基础类型的默认值。例如,在int[]数组的复制过程中,新数组中多余的位置上会被填充“0”。
2)ArraysSupport.newLength(int oldLength, int minGrowth, int prefGrowth)方法。
该方法同样是一个工具性质的方法,主要用于帮助数组在扩容前在不同的场景中找到新的数组容量,并且防止新的数组容量超过系统规定的数组容量上限。该方法的参数如下。
• oldLength:扩容前的数组容量。
• minGrowth:最小的容量增量(必须为正数)。
• prefGrowth:常规的容量增量,该值需要大于minGrowth的值,否则会被忽略。
在计算扩容后数组容量的过程中,如果prefGrowth的值大于minGrowth的值,则以prefGrowth的值计算扩容后的新容量,否则以minGrowth的值计算扩容后的新容量。
如果计算得到的新容量大于系统规定的数组容量上限(使用MAX_ARRAY_LENGTH常量表示,在64位Windows操作系统中该值为2 147 483 639),则需要进行容量限制处理(在实际工作中很少出现这样的情况,但必须考虑到)。
在进行容量限制处理时,如果通过minGrowth(最小容量增加值)计算得到的新容量值小于常量MAX_ARRAY_LENGTH的值,则返回常量MAX_ARRAY_LENGTH的值,否则返回Integer.MAX_VALUE的值。
1.2.2 Vector集合的修改方法——set(int, E)
set(int, E)方法主要用于在指定索引位上设置新的数据对象,设置的数据对象可以为null。该方法有两个参数,第一个参数为int型数据,表示索引位;第二个参数为需要在这个索引位上设置的新的数据对象。
set(int, E)方法有以下几个关键点。
• 可以指定索引位的有效范围上限。不是依据当前Vector集合中elementData数组大小的值,而是依据当前Vector集合中存在的数据量elementCount的值(elementCount的值在Vector集合中的另一个含义就是Vector集合的大小)。
• 该方法有一个返回值,这个返回值会向调用者返回指定索引位上变更之前的值。
1.2.3 Vector集合的删除方法——removeElementAt(int)
removeElementAt(int)方法主要用于移除Vector集合中elementData数组指定索引位上的数据对象,并且改变其索引位的指向。在操作者看来,这个操作可以成功移除X号索引位上的数据对象(X<elementCount),并且在操作成功后,操作者虽然依旧可以通过X号索引位取得数据对象(X<elementCount),但此时取得的是与原数据对象紧邻的数据对象,如图1-11所示。
图1-11
图1-11展示了removeElementAt(int)方法的运行实质:以当前指定的索引位为起点,将后续数据对象的索引位依次向前移动。该方法的源码如下。
首先讲解上述源码中的System.arraycopy(Object src, int srcPos, Object dest, int destPos,int length)方法。该方法是一种JNI native方法,是JDK提供的用于进行两个数组中数据对象复制的性能最好的方法之一。该方法的参数如下。
• src:该参数只能传入数组,表示当前进行数组复制的源数组。
• srcPos:表示在源数组中进行复制操作的起始位置。
• dest:该参数同样只能传入数组,表示当前进行数组复制的目标数组。
• destPos:表示在目标数组中进行复制操作的起始位置。
• length:用于指定进行复制操作的长度。
上述源码使用System.arraycopy()方法的意图如图1-12所示。
图1-12
在图1-12中,虽然完成了数组自身的数据移动,但这时数组中最后一个索引位上的数据对象并没有改变,所以需要手动减小数组中的数据值,并且手动设置最后一个索引位上的数据对象为null。所以上述源码中会出现如下内容。
读者应该已经注意到了,源码片段中多次出现对elementCount变量的操作,这个变量实际上对CAS思想进行了借鉴,本书将在后续相关章节进行介绍。