Java中的Vector详细解读
作者:姚舜禹_12140
一、Vector介绍
Vector是实现了List接口的子类,其底层是一个对象数组,维护了一个elementData数组,是线程安全的,Vector类的方法带有synchronized关键字,在开发中考虑线程安全中使用Vector。
二、源码解析
1、Vector实现的接口
如下图所示:
观察上图,发现Vector继承了AbstractList<E>,并实现了三个接口,分别是:Serializable、Cloneable和RandomAccess。下面对Vector中继承的类和实现的接口进行简单的介绍:
- AbstractList类:该类实现了List接口里面的方法,并且为其提供了默认代码实现。而List接口中主要定义了集合常用的方法让ArrayList进行实现,如:add、addAll、contains、remove、size、indexOf等方法。
- Serializable接口:主要用于序列化,即:能够将对象写入磁盘。与之对应的还有反序列化操作,就是将对象从磁盘中读取出来。因此如果要进行序列化和反序列化,ArrayList的实例对象就必须实现这个接口,否则在实例化的时候程序会报错(java.io.NotSerializableException)。
- Cloneable接口:实现Cloneable接口的类能够调用clone方法,如果没有实现Cloneable接口就调用方法,就会抛出异常(java.lang.CloneNotSupportedException)。
- RandomAccess接口:该接口表示可以随机访问ArrayList当中的数据。随机访问是指我们可以在常量时间复杂度内进行数据的方法,因为ArrayList的底层实现是数组,而数组是可以随机访问的。
2、Vector的构造方法
(1)无参构造方法
public Vector() { this(10); }
总结:Vector的无参构造方法中会调用有参构造方法,创建一个内部数组,该内部数组的初始容量为10,增量为0
(2)带初始容量的构造方法
public Vector(int initialCapacity) { this(initialCapacity, 0); }
总结:构造一个内部数组,该数组的容量为指定的容量,增量为0。
(3)带初始容量和增量的构造方法
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; }
总结:构造一个具有初始容量和增量的数组。
(4)集合型构造方法
public Vector(Collection<? extends E> c) { elementData = c.toArray(); elementCount = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, elementCount, Object[].class); }
总结:构造指定集合元素的数组。
3、Vector中的变量
Vector底层也是数组实现的,其主要变量有:
- elementData::Vector是基于数组的一个实现,elementData就是底层的数组
- elementCount:数组元素个数
- capacityIncrement:指定Vector容量不足的扩容量,不指定的情况下默认翻倍
4、Vector主要方法解析
(1)add方法解析
从上面对于Vector构造方法的分析,不难发现Vector和ArrayList的默认初始容量都是10。那么,我们看看Vector的add()方法又是如何实现的?
public synchronized boolean add(E e) { //AbstractList中的变量 modCount++; //确保数组容量是否足够 ensureCapacityHelper(elementCount + 1); //把元素添加到数组中 elementData[elementCount++] = e; return true; }
可以看到:add方法添加一个元素到列表的末尾。它首先通过ensureCapacityHelper(elemetnCount+1)来保证Object[]数组有足够的空间存放添加的数据,然后再将添加的数据存放到数组对应位置上。
private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
通过ensureCapacityHelper()方法判断最小容量和当前数组长度,若所需的最小容量大于数组大小,则需要进行扩容,然后调用grow()方法实现扩容。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { //旧数组的长度 int oldCapacity = elementData.length; //如果指定了增量,则新数组长度=旧数组长度+增量;如果没有指定容量,则新数组长度=旧数组长度2倍 int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); //判断新容量减去最小容量是否小于0,如果是第一次调用add,则必然小于 if (newCapacity - minCapacity < 0) //将最小容量赋给新容量 newCapacity = minCapacity; /判断新容量减去最大数组大小是否大于0,如果时则计算出一个超大容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); //调用数组工具类方法,创建一个新数组,将新数组的地址赋值给elementData elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { //如果最小容量小于0,抛出异常;否则就比较并返回 if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
Step1:先将当前数组大小赋值给oldCapacity,然后判断是否有指定增量的值,如果有,则新数组长度=旧数组长度+增量;如果没有,则新数组长度=旧数组长度*2。
Step2:利用newCapacity进行两次判断:
第一次判断 if (newCapacity - minCapacity < 0),判断扩容后容量是否大于minCapacity,若小于minCapacity,则直接将minCapacity赋值给newCapacity第二次判断 if (newCapacity - MAX_ARRAY_SIZE > 0),判断newCapacity 是否超出了ArrayList所定义的最大容量,若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为 Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
Step3:最终得到newCapacity,然后调用Arrays.copyOf()方法进行扩容
综合上述分析,有以下几个点:
- Vector如果不指定初始容量,则默认创建一个长度为10的数组。
- Vector如果不指定初始增量,则扩容机制为:新数组长度=旧数组长度*2;如果指定初始增量,则扩容机制为:新数组长度=旧数组长度+增量。
- Vector的add()方法是加了synchronized关键字的,这就意味着它是线程安全的。
接下来可以看看如何在指定位置添加元素:
//在指定位置添加元素 public void add(int index, E element) { insertElementAt(element, index); } //添加元素到指定位置 public synchronized void insertElementAt(E obj, int index) { modCount++; //检查位置合法性 if (index > elementCount) { throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount); } //判断是否需要扩容 ensureCapacityHelper(elementCount + 1); //拷贝数组 System.arraycopy(elementData, index, elementData, index + 1, elementCount - index); //添加元素 elementData[index] = obj; elementCount++; }
可以看到:在指定位置添加元素,首先进行了数组范围的检查,防止越界,然后调用方法检验是否要扩容,且增量++,之后完成数组拷贝即可。
(2)remove()方法
public boolean remove(Object o) { return removeElement(o); } public synchronized boolean removeElement(Object obj) { modCount++; //获取该元素所在的数组下标 int i = indexOf(obj); //如果该元素存在,则移除元素 if (i >= 0) { removeElementAt(i); return true; } return false; } public synchronized void removeElementAt(int index) { modCount++; //如果下标越界,抛出异常 if (index >= elementCount) { throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); } else if (index < 0) { //下标小于0,抛出异常 throw new ArrayIndexOutOfBoundsException(index); } //删除元素 int j = elementCount - index - 1; if (j > 0) { System.arraycopy(elementData, index + 1, elementData, index, j); } elementCount--; elementData[elementCount] = null; /* to let gc do its work */ }
由上述分析:删除元素同样需要进行范围校验。然后计算删除需要移动的数据,再通过数组拷贝移动数组。其次还有一个小细节,可以发现remove()方法是有返回值的,而这个返回值就是我们删除的元素的值。同样的,真正移除元素的remove()方法也是加锁了的。
(3)set()方法
该方法加了synchronized关键字保证安全性,用来设置指定下标的数据,进行元素数据的更新。
public synchronized E set(int index, E element) { //判断合法性 if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); //修改旧值 E oldValue = elementData(index); elementData[index] = element; return oldValue; }
(4) get()方法
该方法用来获取对应下标的数据,也是加锁的方法。
public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); }
(5)其他方法
Vector中的其他方法,如:判断是否为空、获取长度等方法,都是加了锁的。因此,可以认为Vector是线程安全的。
关于Vector中的迭代器,这里不再赘述,有兴趣的可以看这篇文章,里面对Itr进行了介绍:深入理解ArrayList
三、总结
1、Vector简单总结
- Vector的底层实现也是数组,在不指定初始容量的情况下,默认初始数组大小为10,其扩容机制为:当指定了增量的时候,新扩容的容量=旧数组长度+容量;如果没有指定增量,新扩容容量=旧数组长度*2。
- Vector是线程安全的,因为它对很多方法都加锁了。
- Vector和ArrayList都是数组实现的,因此其支持快速随机访问,但增加元素和删除元素的操作却是比较耗时的。
2、对比ArrayList
相同:两个类都实现了List接口,它们都是有序且元素可重复的集合。
不同:
(1)ArrayList 是线程不安全的,Vector 是线程安全的。ArrayList 是线程不安全的,所以当我们不需要保证线程安全性的时候推荐使用 ArrayList,如果想要在多线程中使用 ArrayList 可以通过 Collections.synchronizedList(new ArrayList()) 或 new CopyOnWriteArrayList 的方式创建一个线程安全的 ArrayList 集合;Vector 类的所有方法都是同步的。可以有两个线程安全的访问一个 Vector 对象,但是一个线程访问 Vector 的话会在同步操作上耗费大量的时间。
(2)ArrayList 使用默认构造器创建对象时是在调用 add() 方法时对 ArrayList 的默认容量进行初始化的,Vector 在调用构造器时就对容量进行了初始化
(3)ArrayList 存储数据的 Object 数组使用了transient关键字,Vector 的 Object 数组没有。
关于transient关键字的说明:如果用 transient 声明一个实例变量,当对象存储时,它的值不需要维持。这里的对象存储是指,Java 的 serialization 提供的一种持久化对象实例的机制。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。例如:当持久化对象时,可能有一个特殊的对象数据成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的一个域上关闭 serialization,可以在这个域前加上关键字 transient。
简单的说,就是被 transient 修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null 。
(4)扩容机制不同。ArrayList扩容机制为变为原来的1.5倍,而Vector扩容时如果指定了增量,则新数组长度=旧数组长度+增量,如果没有指定,就扩容为原来的2倍。
到此这篇关于Java中的Vector详细解读的文章就介绍到这了,更多相关Vector解读内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!