本文对Java中的Vector集合的源码进行了深度解析,包括各种方法、特有的Enumeration迭代器机制,并且给出了Vector和ArrayList的区别以及使用建议。
public class Vector
extends AbstractList
implements List, RandomAccess, Cloneable, Serializable
Vector,来自于JDK1.0 的古老集合类,继承自 AbstractList,实现了 List 接口 ,底层是数组结构,元素可重复,有序(存放顺序),支持下标索引访问,允许null元素。
该类当中所有方法的实现都是同步的,方法采用了synchronized修饰,数据安全,效率低!可以看成ArrayList的同步版本,但是并不完全相同,比如迭代器。
实现了 RandomAccess标志性接口,这意味着这个集合支持 快速随机访问 策略,那么使用传统for循环的方式遍历数据会优于用迭代器遍历数据,即使用get(index)方法获取数据相比于迭代器遍历更加快速!
还实现了Cloneable、Serializable两个标志性接口,所以Vector支持克隆、序列化。
Vector的方法的原理和ArrayList非常相似,因此如果了解AarrayList那么理解Vector将会很简单!对于某些方法,本文在ArrayList集合中会有详细介绍,因此强烈建议先学习ArrayList:Java的ArrayList集合源码深度解析以及应用介绍。
Vector的底层数据结构就是一个数组,数组元素的类型为Object类型,对Vector的所有操作底层都是基于数组的。
初始容量:
调用空构造器时,立即初始化为10个容量的数组,也可以指定初始容量。
加载因子:
1,即存放数据时,如果存放数据后的容量大于底层数组的容量,那么首先扩容。
扩容增量:
新容量默认增加原容量的1倍,但是也可以在构造器指定扩容时的容量增量!如果新容量还是小于最小容量,则新容量还是等于最小容量!
相比于ArrayList,属性还是很简单的,多了一个capacityIncrement。
/**
* 存放元素的底层容器,就是一个数组,当前数组的长度就是Vector的容量
*/
protected Object[] elementData;
/**
* 元素的个数
*/
protected int elementCount;
/**
* 当vector的大小大于其容量时,其容量扩充的大小,即容量增量。如果容量增量小于或等于零,则每次需要增长时vector的容量都会加倍。
*/
protected int capacityIncrement;
构造一个空集合,使其内部数据数组的大小初始化为10,容量增量为零。其源码为:
public Vector() {
this(10);
}
可以看到调用了另外一个构造方法。
构造具有指定初始容量并且其容量增量等于零的空集合。
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
可以看到调用了另外一个构造方法。
构造具有指定的初始容量和容量增量的空集合。
public Vector(int initialCapacity, int capacityIncrement) {
//调用父类构造器
super();
//检查初始化容量,如果小于0就抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//初始化指定容量的数组
this.elementData = new Object[initialCapacity];
//初始化容量增量
this.capacityIncrement = capacityIncrement;
}
构造一个包含指定 collection 中的元素的集合,这些元素按其 collection 的迭代器返回元素的顺序排列。
public Vector(Collection<? extends E> c) {
//获取集合的元素数组,赋值给elementData
elementData = c.toArray();
//获取此时集合的容量,赋值给elementCount
elementCount = elementData.length;
if (elementData.getClass() != Object[].class)
//如果新加入的数组不是object[]类型的数组,则转换为object[]类型的数组
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
add方法的原理基本和ArrayList的add方法一致,在ArrayList的文章中有详细介绍,这里不再赘述。Java的ArrayList集合源码深度解析以及应用介绍。
/**
* 加了synchronized的同步方法
* @param e 需要添加的元素
* @return 添加成功,返回true
*/
public synchronized boolean add(E e) {
//集合结构修改次数自增1
modCount++;
//确保数组容量够用,最小容量为当前元素个数+1
ensureCapacityHelper(elementCount + 1);
//添加元素
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
//如果最小容量减去数组的长度的值大于0
if (minCapacity - elementData.length > 0)
//那么有可能是需要扩容,或者数组长度移除
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
//获取老的容量
int oldCapacity = elementData.length;
//如果容量增量大于0,增新容量为老容量加上容量增量,否则新容量是老容量的两倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
//如果此时新容量减去老容量的值还是小于0,那么新容量等于最小容量,或者数组长度溢出
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果此时新容量减去建议最大容量的值还是小于0,那么新容量等于最小容量,或者数组长度溢出
//此时需要抛出异常或者重新分配新容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
//抛出异常或者重新分配新容量
newCapacity = hugeCapacity(minCapacity);
//数组拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 和arraylist是同样的逻辑
* @param minCapacity
* @return
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
public synchronized boolean addAll(Collection<? extends E> c) {
//结构改变+1
modCount++;
//获取加入集合的元素数组
Object[] a = c.toArray();
//获取加入的元素的数量
int numNew = a.length;
//确保容量能够容纳这些元素
ensureCapacityHelper(elementCount + numNew);
//元素的拷贝存放
System.arraycopy(a, 0, elementData, elementCount, numNew);
//元素数量增加
elementCount += numNew;
//如果此集合由于调用而更改了结构,即numNew>0,则返回 true
return numNew != 0;
}
public E remove(int index)
移除此集合中指定索引位置上的元素,向左移动所有后续元素(将其索引减1),并返回此集合中移除的元素。
从源码中可以看到,需要调用System.arraycopy() 将删除元素 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素和扩容一样,代价是非常高的。
remove的源码,还是比较简单的:
public synchronized E remove(int index) {
modCount++;
//检查要移除的元素索引是否越界(上界)
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
//获取该索引处的元素
E oldValue = elementData(index);
//要移动的数据长度elementCount-(index + 1) 最小值0最大值size-1
int numMoved = elementCount - index - 1;
if (numMoved > 0)
//将index+1后面的列表对象前移一位,该操作将会覆盖index以及之后的元素,相当于删除了一位元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 数组前移一位,size自减-,空出来的位置(原数组的有效数据的最后一位)置null,原来的具体的对象的销毁由Junk收集器负责
elementData[--elementCount] = null;
//返回被移除的元素
return oldValue;
}
public E get(int index)
返回此集合中指定索引位置上的元素。
public synchronized E get(int index) {
//检查要获取的元素索引是否越界(上界)
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
//返回索引处的元素
return elementData(index);
}
public E set(int index,E element)
用指定的元素替代此列表中指定索引位置上的元素。
public synchronized E set(int index, E element) {
//检查要设置的元素索引是否越界(上界)
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
//获取旧的值
E oldValue = elementData(index);
//替换值
elementData[index] = element;
//返回旧值
return oldValue;
}
返回的是一个全新的Vector实例对象,但是其elementData,也就是存储数据的数组,存储的对象还是指向了旧的Vector存储的那些对象。也就是Vector这个类实现了深拷贝,但是对于存储的对象还是浅拷贝。
public synchronized Object clone() {
try {
@SuppressWarnings("unchecked")
//克隆集合对象
Vector<E> v = (Vector<E>) super.clone();
//克隆内部数组,导致虽然数组的引用不一样,但是但是数组内部的相同索引处的元素引用指向同一个堆内存地址,即还是同一个对象
v.elementData = Arrays.copyOf(elementData, elementCount);
//设置新集合的modCount为0
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
同ArrayList一样,Vector也有自己的writeObject方法,区别是Vector的内部数组被全部序列化存储了,包括没有使用道的部分,而ArrayList的内部数组没有全部进行序列化,只是序列化了储存了元素的部分。
并且Vector没有实现readObject方法,那么将会按照默认的方式进行反序列化。
writeObject:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
final java.io.ObjectOutputStream.PutField fields = s.putFields();
final Object[] data;
synchronized (this) {
fields.put("capacityIncrement", capacityIncrement);
fields.put("elementCount", elementCount);
//直接对整个数组进行了序列化,相比于ArrayList代码更加简单,但是更占用空间
data = elementData.clone();
}
fields.put("elementData", data);
s.writeFields();
}
由于Vector属于List接口集合体系,因此具有通用的iterator 和 listIterator迭代器(关于这两个迭代器在ArrayList文章部分有详细讲解,这里不再赘述)。但是我们知道List接口是JDK1.2的时候加进来的,但是Vector在JDK1.0的时候就出现了,因此Vector还具有自己独有迭代器Enumeration,也被称为“枚举”!
Enumeration原本是一个接口,实现Enumeration接口的对象,又称为枚举对象。通过它的api方法可以遍历Vector集合的元素,但是该接口不属于集合体系,并且JDK的API给出了如下建议:此接口的功能与Iterator接口的功能是重复的。此外,Iterator 接口添加了一个可选的移除操作,并使用较短的方法名。新的实现应该优先考虑使用 Iterator 接口而不是 Enumeration 接口。
由于Enumeration迭代器比较古老,因此功能也很简略,并没有iterator 和 listIterator迭代器的快速失败机制,因此可能出现一些不会抛出异常的“异常情况”,比如下面的代码:
/**
* Enumeration迭代器的死循环
*/
@Test
public void test2() {
Vector<Integer> vector = new Vector<>(Collections.singletonList(1));
//获取自己的迭代器Enumeration
Enumeration elements = vector.elements();
int j = 0;
//是否存在更多元素
while (elements.hasMoreElements()) {
//内部采用集合的方法添加元素,如果是iterator 和 listIterator 迭代器,那么会马上抛出ConcurrentModificationException异常
//但是由于Enumeration迭代器,没有这个功能,因此会导致死循环,直到OOM
vector.add(j++);
//获取下一个元素
Object o = elements.nextElement();
//打印元素
System.out.println(o);
}
}
上面的代码将会造成死循环!
public Enumeration elements()
返回此集合的枚举。返回的 Enumeration 对象将具有此集合中的所有元素。第一项为索引0处的元素,然后是索引1处的元素,依此类推。
下面是源码:
public Enumeration<E> elements() {
//和iterator 与 listIterator迭代器不一样
//Enumeration迭代器甚至简陋得"没有自己的内部类实现"
//这里返回的就是一个匿名内部类,即返回一次实现一次。
return new Enumeration<E>() {
//用于遍历元素个数的计数
int count = 0;
//是否还存在元素 通过比较count和elementCount的值
//如果遍历的元素个数小于外部集合的元素个数,那就说明有元素,可以获取,返回true
public boolean hasMoreElements() {
return count < elementCount;
}
//返回下一个元素
public E nextElement() {
synchronized (Vector.this) {
//如果遍历的元素个数小于外部集合的元素个数,那就说明有元素,可以获取
if (count < elementCount) {
//返回该遍历的元素个数索引处的元素,同时遍历的元素个数自增1
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}
我们看到,Enumeration的实现非常简单,返回的是一个匿名内部类,并没有“并发修改”的检测,并且内部只提供了两个方法hasMoreElements()和nextElement()方法。并没有add、remove等修改集合元素方法,功能更加简陋!
boolean hasMoreElements()
当且仅当此枚举对象至少还包含一个可提供的元素时,才返回 true;否则返回 false。
E nextElement()
如果此枚举对象至少还有一个可提供的元素,则返回此枚举的下一个元素。
我们来分析上面的案例是如何导致死循环的!
首先获取迭代器,该迭代器和外部集合共用一个内部数组。
进入循环,hasMoreElements()判断是否存在下一个元素,由于原集合存在一个元素,因此count=0 < elementCount=1,即返回true。
然后循环体内部,首先对外部集合添加了元素,这会导致elementCount++,变成2。继续nextElement()方法,同样判断存在元素,因此返回elementData(0++),即返回elementData[0],然后count自增一变成1,第一次循环结束。
第二次循环,这是hasMoreElements()发现count=1 < elementCount=2,即返回true。
然后循环体内部,又首先对外部集合添加了元素,这会导致elementCount++,变成3。继续nextElement()方法,同样判断存在元素,因此返回elementData(1++),即返回elementData[1],然后count自增一变成2,第二次循环结束。
我们发现,无论怎么循环,count始终小于elementCount,这就是导致死循环的原因,在开发过程如果要使用Vector集合,那么要避免这种情况,并且最好是不去使用老旧的Enumeration迭代器!
/**
1. 使用枚举遍历ArrayList
*/
@Test
public void test3() {
ArrayList<String> al = new ArrayList<>(Arrays.asList("b", "a", "s", "c", "11", null));
Vector<String> v = new Vector<>(al);
Enumeration<String> elements = v.elements();
while (elements.hasMoreElements()) {
String s = elements.nextElement();
System.out.println(s);
}
}
Vector和ArrayList的异同点:
相同点:
不同点:
Vector使用建议:
Vector集合的方法都加了snchronized,因此效率较低,如果想要使用线程安全的List集合,那么可以使用Collections.synchronizedList()得到一个线程安全的List,实际上它内部使用snchronized关键字修饰的代码块,效率也很一般,推荐使用JUC包下的 CopyOnWriteArrayList类,效率比较高,但是它的“写时复制”机制可能会造成数据不同步。总之,面试中,Vector被问到的概率比较小,而在开发过程中,不推荐使用Vector集合。
我们后续将会介绍的更多集合,比如LinkedList、TreeMap、HashMap,LinkedHashMap等基本集合以及JUC包中的高级并发集合。如果想学习集合源码的关注我的更新!
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!